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 096dd4673faa..c73d4b10d4c5 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -91,7 +91,6 @@ import com.duckduckgo.app.browser.commands.NavigationCommand import com.duckduckgo.app.browser.commands.NavigationCommand.Navigate import com.duckduckgo.app.browser.customtabs.CustomTabPixelNames import com.duckduckgo.app.browser.defaultbrowsing.prompts.AdditionalDefaultBrowserPrompts -import com.duckduckgo.app.browser.defaultbrowsing.prompts.ui.experiment.OnboardingHomeScreenWidgetExperiment import com.duckduckgo.app.browser.duckplayer.DUCK_PLAYER_FEATURE_NAME import com.duckduckgo.app.browser.duckplayer.DUCK_PLAYER_PAGE_FEATURE_NAME import com.duckduckgo.app.browser.duckplayer.DuckPlayerJSHelper @@ -122,6 +121,7 @@ import com.duckduckgo.app.browser.santize.NonHttpAppLinkChecker import com.duckduckgo.app.browser.session.WebViewSessionStorage import com.duckduckgo.app.browser.tabs.TabManager import com.duckduckgo.app.browser.trafficquality.AndroidFeaturesHeaderPlugin.Companion.X_DUCKDUCKGO_ANDROID_HEADER +import com.duckduckgo.app.browser.ui.dialogs.widgetprompt.OnboardingHomeScreenWidgetToggles import com.duckduckgo.app.browser.viewstate.BrowserViewState import com.duckduckgo.app.browser.viewstate.CtaViewState import com.duckduckgo.app.browser.viewstate.FindInPageViewState @@ -561,7 +561,7 @@ class BrowserTabViewModelTest { private val mockSiteHttpErrorHandler: HttpCodeSiteErrorHandler = mock() private val mockSubscriptionsJSHelper: SubscriptionsJSHelper = mock() private val mockReactivateUsersExperiment: ReactivateUsersExperiment = mock() - private val mockOnboardingHomeScreenWidgetExperiment: OnboardingHomeScreenWidgetExperiment = mock() + private val mockOnboardingHomeScreenWidgetToggles: OnboardingHomeScreenWidgetToggles = mock() private val mockRebrandingFeatureToggle: SubscriptionRebrandingFeatureToggle = mock() private val tabManager: TabManager = mock() @@ -674,7 +674,7 @@ class BrowserTabViewModelTest { subscriptions = subscriptions, duckPlayer = mockDuckPlayer, brokenSitePrompt = mockBrokenSitePrompt, - onboardingHomeScreenWidgetExperiment = mockOnboardingHomeScreenWidgetExperiment, + onboardingHomeScreenWidgetToggles = mockOnboardingHomeScreenWidgetToggles, onboardingDesignExperimentManager = mockOnboardingDesignExperimentManager, rebrandingFeatureToggle = mockRebrandingFeatureToggle, ) 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 34c2c79cb4fe..0ec76f94ef70 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 @@ -25,7 +25,7 @@ import androidx.room.Room import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.browser.DuckDuckGoUrlDetectorImpl import com.duckduckgo.app.browser.R -import com.duckduckgo.app.browser.defaultbrowsing.prompts.ui.experiment.OnboardingHomeScreenWidgetExperiment +import com.duckduckgo.app.browser.ui.dialogs.widgetprompt.OnboardingHomeScreenWidgetToggles import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.model.DismissedCta @@ -60,6 +60,7 @@ 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.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.subscriptions.api.SubscriptionRebrandingFeatureToggle import com.duckduckgo.subscriptions.api.Subscriptions @@ -121,7 +122,7 @@ class CtaViewModelTest { private val mockBrokenSitePrompt: BrokenSitePrompt = mock() - private val mockOnboardingHomeScreenWidgetExperiment: OnboardingHomeScreenWidgetExperiment = mock() + private val fakeOnboardingHomeScreenWidgetToggles = FakeFeatureToggleFactory.create(OnboardingHomeScreenWidgetToggles::class.java) private val mockOnboardingDesignExperimentManager: OnboardingDesignExperimentManager = mock() @@ -183,7 +184,7 @@ class CtaViewModelTest { subscriptions = mockSubscriptions, duckPlayer = mockDuckPlayer, brokenSitePrompt = mockBrokenSitePrompt, - onboardingHomeScreenWidgetExperiment = mockOnboardingHomeScreenWidgetExperiment, + onboardingHomeScreenWidgetToggles = fakeOnboardingHomeScreenWidgetToggles, onboardingDesignExperimentManager = mockOnboardingDesignExperimentManager, rebrandingFeatureToggle = mockRebrandingFeatureToggle, ) @@ -356,7 +357,8 @@ class CtaViewModelTest { fun whenRefreshCtaOnHomeTabAndHideTipsIsTrueAndWidgetCompatibleThenReturnWidgetCta() = runTest { whenever(mockSettingsDataStore.hideTips).thenReturn(true) whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(true) - whenever(mockOnboardingHomeScreenWidgetExperiment.isOnboardingHomeScreenWidgetExperiment()).thenReturn(false) + fakeOnboardingHomeScreenWidgetToggles.self().setRawStoredState(Toggle.State(true)) + fakeOnboardingHomeScreenWidgetToggles.onboardingHomeScreenWidgetPrompt().setRawStoredState(Toggle.State(false)) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false, detectedRefreshPatterns = detectedRefreshPatterns) assertTrue(value is HomePanelCta.AddWidgetAuto) @@ -382,7 +384,8 @@ class CtaViewModelTest { whenever(mockSettingsDataStore.hideTips).thenReturn(true) whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(true) whenever(mockWidgetCapabilities.hasInstalledWidgets).thenReturn(false) - whenever(mockOnboardingHomeScreenWidgetExperiment.isOnboardingHomeScreenWidgetExperiment()).thenReturn(false) + fakeOnboardingHomeScreenWidgetToggles.self().setRawStoredState(Toggle.State(true)) + fakeOnboardingHomeScreenWidgetToggles.onboardingHomeScreenWidgetPrompt().setRawStoredState(Toggle.State(false)) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false, detectedRefreshPatterns = detectedRefreshPatterns) assertTrue(value is HomePanelCta.AddWidgetAuto) @@ -393,7 +396,8 @@ class CtaViewModelTest { whenever(mockSettingsDataStore.hideTips).thenReturn(true) whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(true) whenever(mockWidgetCapabilities.hasInstalledWidgets).thenReturn(false) - whenever(mockOnboardingHomeScreenWidgetExperiment.isOnboardingHomeScreenWidgetExperiment()).thenReturn(true) + fakeOnboardingHomeScreenWidgetToggles.self().setRawStoredState(Toggle.State(true)) + fakeOnboardingHomeScreenWidgetToggles.onboardingHomeScreenWidgetPrompt().setRawStoredState(Toggle.State(true)) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false, detectedRefreshPatterns = detectedRefreshPatterns) assertTrue(value is HomePanelCta.AddWidgetAutoOnboardingExperiment) @@ -897,7 +901,8 @@ class CtaViewModelTest { whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO_VISIT_SITE)).thenReturn(true) whenever(mockDismissedCtaDao.exists(CtaId.DAX_END)).thenReturn(true) whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(true) - whenever(mockOnboardingHomeScreenWidgetExperiment.isOnboardingHomeScreenWidgetExperiment()).thenReturn(false) + fakeOnboardingHomeScreenWidgetToggles.self().setRawStoredState(Toggle.State(true)) + fakeOnboardingHomeScreenWidgetToggles.onboardingHomeScreenWidgetPrompt().setRawStoredState(Toggle.State(false)) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = false, detectedRefreshPatterns = detectedRefreshPatterns) assertFalse(value is DaxBubbleCta.DaxPrivacyProCta) 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 714374869258..26855dfbde73 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -124,7 +124,6 @@ import com.duckduckgo.app.browser.customtabs.CustomTabPixelNames import com.duckduckgo.app.browser.customtabs.CustomTabViewModel.Companion.CUSTOM_TAB_NAME_PREFIX import com.duckduckgo.app.browser.databinding.FragmentBrowserTabBinding import com.duckduckgo.app.browser.databinding.HttpAuthenticationBinding -import com.duckduckgo.app.browser.defaultbrowsing.prompts.ui.experiment.ExperimentalHomeScreenWidgetBottomSheetDialog import com.duckduckgo.app.browser.downloader.BlobConverterInjector import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.browser.filechooser.FileChooserIntentBuilder @@ -159,6 +158,7 @@ import com.duckduckgo.app.browser.tabpreview.WebViewPreviewGenerator import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister import com.duckduckgo.app.browser.ui.dialogs.AutomaticFireproofDialogOptions import com.duckduckgo.app.browser.ui.dialogs.LaunchInExternalAppOptions +import com.duckduckgo.app.browser.ui.dialogs.widgetprompt.AlternativeHomeScreenWidgetBottomSheetDialog import com.duckduckgo.app.browser.urlextraction.DOMUrlExtractor import com.duckduckgo.app.browser.urlextraction.UrlExtractingWebView import com.duckduckgo.app.browser.urlextraction.UrlExtractingWebViewClient @@ -603,7 +603,7 @@ class BrowserTabFragment : private lateinit var popupMenu: BrowserPopupMenu private lateinit var ctaBottomSheet: PromoBottomSheetDialog - private lateinit var experimentalBottomSheet: ExperimentalHomeScreenWidgetBottomSheetDialog + private lateinit var widgetBottomSheetDialog: AlternativeHomeScreenWidgetBottomSheetDialog private lateinit var autoCompleteSuggestionsAdapter: BrowserAutoCompleteSuggestionsAdapter @@ -4646,23 +4646,23 @@ class BrowserTabFragment : private fun showBottomSheetCta(configuration: HomePanelCta) { if (configuration is AddWidgetAutoOnboardingExperiment) { - showExperimentalHomeWidget(configuration) + showAlternativeHomeWidgetPrompt(configuration) } else { showHomeCta(configuration) } } - private fun showExperimentalHomeWidget( + private fun showAlternativeHomeWidgetPrompt( configuration: HomePanelCta, ) { hideDaxCta() - if (!::experimentalBottomSheet.isInitialized) { - experimentalBottomSheet = ExperimentalHomeScreenWidgetBottomSheetDialog( + if (!::widgetBottomSheetDialog.isInitialized) { + widgetBottomSheetDialog = AlternativeHomeScreenWidgetBottomSheetDialog( context = requireContext(), isLightModeEnabled = appTheme.isLightModeEnabled(), ) - experimentalBottomSheet.eventListener = object : ExperimentalHomeScreenWidgetBottomSheetDialog.EventListener { + widgetBottomSheetDialog.eventListener = object : AlternativeHomeScreenWidgetBottomSheetDialog.EventListener { override fun onShown() { viewModel.onCtaShown() } @@ -4679,10 +4679,10 @@ class BrowserTabFragment : viewModel.onUserClickCtaSecondaryButton(configuration) } } - experimentalBottomSheet.show() + widgetBottomSheetDialog.show() } else { - if (!experimentalBottomSheet.isShowing) { - experimentalBottomSheet.show() + if (!widgetBottomSheetDialog.isShowing) { + widgetBottomSheetDialog.show() } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/experiment/OnboardingHomeScreenWidgetExperiment.kt b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/experiment/OnboardingHomeScreenWidgetExperiment.kt deleted file mode 100644 index d92769a4ba4e..000000000000 --- a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/experiment/OnboardingHomeScreenWidgetExperiment.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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.browser.defaultbrowsing.prompts.ui.experiment - -import com.duckduckgo.app.browser.defaultbrowsing.prompts.ui.experiment.OnboardingHomeScreenWidgetToggles.Cohorts.CONTROL -import com.duckduckgo.app.browser.defaultbrowsing.prompts.ui.experiment.OnboardingHomeScreenWidgetToggles.Cohorts.VARIANT_ONBOARDING_HOME_SCREEN_WIDGET_PROMPT -import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.app.widget.experiment.store.WidgetSearchCountDataStore -import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.feature.toggles.api.MetricsPixel -import com.duckduckgo.feature.toggles.api.PixelDefinition -import com.squareup.anvil.annotations.ContributesBinding -import dagger.SingleInstanceIn -import java.time.LocalDate -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject -import kotlinx.coroutines.withContext - -interface OnboardingHomeScreenWidgetExperiment { - suspend fun enroll() - suspend fun isControl(): Boolean - suspend fun isOnboardingHomeScreenWidgetExperiment(): Boolean - - suspend fun fireOnboardingWidgetDisplay() - suspend fun fireOnboardingWidgetAdd() - suspend fun fireOnboardingWidgetDismiss() - suspend fun fireWidgetSearch() - suspend fun fireWidgetSearchXCount() -} - -@ContributesBinding( - scope = AppScope::class, - boundType = OnboardingHomeScreenWidgetExperiment::class, -) -@SingleInstanceIn(AppScope::class) -class OnboardingHomeScreenWidgetExperimentImpl @Inject constructor( - private val dispatcherProvider: DispatcherProvider, - private val onboardingHomeScreenWidgetToggles: OnboardingHomeScreenWidgetToggles, - private val onboardingHomeScreenWidgetPixelsPlugin: OnboardingHomeScreenWidgetPixelsPlugin, - private val pixel: Pixel, - private val widgetSearchCountDataStore: WidgetSearchCountDataStore, -) : OnboardingHomeScreenWidgetExperiment { - - override suspend fun enroll() { - onboardingHomeScreenWidgetToggles.onboardingHomeScreenWidgetExperimentJun25().enroll() - } - - override suspend fun isControl(): Boolean = - onboardingHomeScreenWidgetToggles.onboardingHomeScreenWidgetExperimentJun25().isEnrolledAndEnabled(CONTROL) - - override suspend fun isOnboardingHomeScreenWidgetExperiment(): Boolean = - onboardingHomeScreenWidgetToggles.onboardingHomeScreenWidgetExperimentJun25().isEnrolledAndEnabled( - VARIANT_ONBOARDING_HOME_SCREEN_WIDGET_PROMPT, - ) - - override suspend fun fireOnboardingWidgetDisplay() { - withContext(dispatcherProvider.io()) { - onboardingHomeScreenWidgetPixelsPlugin.getOnboardingWidgetDisplayMetric()?.fire() - } - } - - override suspend fun fireOnboardingWidgetAdd() { - withContext(dispatcherProvider.io()) { - onboardingHomeScreenWidgetPixelsPlugin.getOnboardingWidgetAddMetric()?.fire() - } - } - - override suspend fun fireOnboardingWidgetDismiss() { - withContext(dispatcherProvider.io()) { - onboardingHomeScreenWidgetPixelsPlugin.getOnboardingWidgetDismissMetric()?.fire() - } - } - - override suspend fun fireWidgetSearch() { - withContext(dispatcherProvider.io()) { - onboardingHomeScreenWidgetPixelsPlugin.getWidgetSearchMetric()?.fire() - } - } - - override suspend fun fireWidgetSearchXCount() { - withContext(dispatcherProvider.io()) { - onboardingHomeScreenWidgetPixelsPlugin.getWidgetSearch3xMetric()?.getPixelDefinitions()?.forEach { definition -> - if (isInConversionWindow(definition)) { - widgetSearchCountDataStore.getMetricForPixelDefinition(definition).takeIf { it < 3 }?.let { - widgetSearchCountDataStore.increaseMetricForPixelDefinition(definition).takeIf { it == 3 }?.apply { - pixel.fire(definition.pixelName, definition.params) - } - } - } - } - - onboardingHomeScreenWidgetPixelsPlugin.getWidgetSearch5xMetric()?.getPixelDefinitions()?.forEach { definition -> - if (isInConversionWindow(definition)) { - widgetSearchCountDataStore.getMetricForPixelDefinition(definition).takeIf { it < 5 }?.let { - widgetSearchCountDataStore.increaseMetricForPixelDefinition(definition).takeIf { it == 5 }?.apply { - pixel.fire(definition.pixelName, definition.params) - } - } - } - } - } - } - - private fun isInConversionWindow(definition: PixelDefinition): Boolean { - val enrollmentDate = definition.params["enrollmentDate"] ?: return false - val lowerWindow = definition.params["conversionWindowDays"]?.split("-")?.first()?.toInt() ?: return false - val upperWindow = definition.params["conversionWindowDays"]?.split("-")?.last()?.toInt() ?: return false - val daysDiff = daysBetweenTodayAnd(enrollmentDate) - - return (daysDiff in lowerWindow..upperWindow) - } - - private fun daysBetweenTodayAnd(date: String): Long { - val today = ZonedDateTime.now(ZoneId.of("America/New_York")) - val localDate = LocalDate.parse(date) - val zoneDateTime: ZonedDateTime = localDate.atStartOfDay(ZoneId.of("America/New_York")) - return ChronoUnit.DAYS.between(zoneDateTime, today) - } - - private fun MetricsPixel.fire() = getPixelDefinitions().forEach { - pixel.fire(it.pixelName, it.params) - } -} diff --git a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/experiment/OnboardingHomeScreenWidgetToggles.kt b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/experiment/OnboardingHomeScreenWidgetToggles.kt deleted file mode 100644 index 6488845b4da3..000000000000 --- a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/experiment/OnboardingHomeScreenWidgetToggles.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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.browser.defaultbrowsing.prompts.ui.experiment - -import com.duckduckgo.anvil.annotations.ContributesRemoteFeature -import com.duckduckgo.app.browser.defaultbrowsing.prompts.ui.experiment.OnboardingHomeScreenWidgetToggles.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.FALSE -import com.duckduckgo.feature.toggles.api.Toggle.DefaultValue -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 OnboardingHomeScreenWidgetToggles { - - @DefaultValue(FALSE) - fun self(): Toggle - - @DefaultValue(FALSE) - fun onboardingHomeScreenWidgetExperimentJun25(): Toggle - - enum class Cohorts(override val cohortName: String) : CohortName { - CONTROL("control"), // current bottom sheet prompt - VARIANT_ONBOARDING_HOME_SCREEN_WIDGET_PROMPT("experimentalOnboardingHomeScreenWidgetPrompt"), // new bottom sheet prompt - } - - companion object { - internal const val BASE_EXPERIMENT_NAME = "onboardingHomeScreenWidget" - } -} - -@ContributesMultibinding(AppScope::class) -@SingleInstanceIn(AppScope::class) -class OnboardingHomeScreenWidgetPixelsPlugin @Inject constructor( - private val toggles: OnboardingHomeScreenWidgetToggles, -) : MetricsPixelPlugin { - - override suspend fun getMetrics(): List { - return listOf( - MetricsPixel( - metric = METRIC_ONBOARDING_WIDGET_DISPLAY, - value = "1", - toggle = toggles.onboardingHomeScreenWidgetExperimentJun25(), - conversionWindow = listOf( - ConversionWindow(lowerWindow = 0, upperWindow = 0), - ), - ), - MetricsPixel( - metric = METRIC_ONBOARDING_WIDGET_ADD, - value = "1", - toggle = toggles.onboardingHomeScreenWidgetExperimentJun25(), - conversionWindow = listOf( - ConversionWindow(lowerWindow = 0, upperWindow = 0), - ), - ), - MetricsPixel( - metric = METRIC_ONBOARDING_WIDGET_DISMISS, - value = "1", - toggle = toggles.onboardingHomeScreenWidgetExperimentJun25(), - conversionWindow = listOf( - ConversionWindow(lowerWindow = 0, upperWindow = 0), - ), - ), - MetricsPixel( - metric = METRIC_WIDGET_SEARCH, - value = "1", - toggle = toggles.onboardingHomeScreenWidgetExperimentJun25(), - conversionWindow = listOf( - ConversionWindow(lowerWindow = 5, upperWindow = 7), - ConversionWindow(lowerWindow = 8, upperWindow = 14), - ), - ), - MetricsPixel( - metric = METRIC_WIDGET_SEARCH_3X, - value = "1", - toggle = toggles.onboardingHomeScreenWidgetExperimentJun25(), - conversionWindow = listOf( - ConversionWindow(lowerWindow = 5, upperWindow = 7), - ), - ), - MetricsPixel( - metric = METRIC_WIDGET_SEARCH_5X, - value = "1", - toggle = toggles.onboardingHomeScreenWidgetExperimentJun25(), - conversionWindow = listOf( - ConversionWindow(lowerWindow = 5, upperWindow = 7), - ), - ), - ) - } - - suspend fun getOnboardingWidgetDisplayMetric(): MetricsPixel? { - return this.getMetrics().firstOrNull { it.metric == METRIC_ONBOARDING_WIDGET_DISPLAY } - } - - suspend fun getOnboardingWidgetAddMetric(): MetricsPixel? { - return this.getMetrics().firstOrNull { it.metric == METRIC_ONBOARDING_WIDGET_ADD } - } - - suspend fun getOnboardingWidgetDismissMetric(): MetricsPixel? { - return this.getMetrics().firstOrNull { it.metric == METRIC_ONBOARDING_WIDGET_DISMISS } - } - - suspend fun getWidgetSearchMetric(): MetricsPixel? { - return this.getMetrics().firstOrNull { it.metric == METRIC_WIDGET_SEARCH } - } - - suspend fun getWidgetSearch3xMetric(): MetricsPixel? { - return this.getMetrics().firstOrNull { it.metric == METRIC_WIDGET_SEARCH_3X } - } - - suspend fun getWidgetSearch5xMetric(): MetricsPixel? { - return this.getMetrics().firstOrNull { it.metric == METRIC_WIDGET_SEARCH_5X } - } - - companion object { - internal const val METRIC_ONBOARDING_WIDGET_DISPLAY = "onboardingWidgetDisplay" - internal const val METRIC_ONBOARDING_WIDGET_ADD = "onboardingWidgetAdd" - internal const val METRIC_ONBOARDING_WIDGET_DISMISS = "onboardingWidgetDismiss" - internal const val METRIC_WIDGET_SEARCH = "widgetSearch" - internal const val METRIC_WIDGET_SEARCH_3X = "widgetSearch3x" - internal const val METRIC_WIDGET_SEARCH_5X = "widgetSearch5x" - } -} diff --git a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/experiment/ExperimentalHomeScreenWidgetBottomSheetDialog.kt b/app/src/main/java/com/duckduckgo/app/browser/ui/dialogs/widgetprompt/AlternativeHomeScreenWidgetBottomSheetDialog.kt similarity index 78% rename from app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/experiment/ExperimentalHomeScreenWidgetBottomSheetDialog.kt rename to app/src/main/java/com/duckduckgo/app/browser/ui/dialogs/widgetprompt/AlternativeHomeScreenWidgetBottomSheetDialog.kt index ac7b497340a1..6e9ffc9f254c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/prompts/ui/experiment/ExperimentalHomeScreenWidgetBottomSheetDialog.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/ui/dialogs/widgetprompt/AlternativeHomeScreenWidgetBottomSheetDialog.kt @@ -14,28 +14,28 @@ * limitations under the License. */ -package com.duckduckgo.app.browser.defaultbrowsing.prompts.ui.experiment +package com.duckduckgo.app.browser.ui.dialogs.widgetprompt import android.annotation.SuppressLint import android.content.Context import android.content.DialogInterface import android.view.LayoutInflater import android.widget.FrameLayout -import com.duckduckgo.app.browser.databinding.BottomSheetExperimentHomeScreenWidgetBinding -import com.google.android.material.R +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.databinding.BottomSheetHomeScreenWidgetBinding import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable @SuppressLint("NoBottomSheetDialog") -class ExperimentalHomeScreenWidgetBottomSheetDialog( +class AlternativeHomeScreenWidgetBottomSheetDialog( private val context: Context, isLightModeEnabled: Boolean, ) : BottomSheetDialog(context) { - private val binding: BottomSheetExperimentHomeScreenWidgetBinding = - BottomSheetExperimentHomeScreenWidgetBinding.inflate(LayoutInflater.from(context)) + private val binding: BottomSheetHomeScreenWidgetBinding = + BottomSheetHomeScreenWidgetBinding.inflate(LayoutInflater.from(context)) var eventListener: EventListener? = null @@ -54,18 +54,18 @@ class ExperimentalHomeScreenWidgetBottomSheetDialog( eventListener?.onCanceled() dismiss() } - binding.experimentHomeScreenWidgetBottomSheetDialogImage.setImageResource( + binding.homeScreenWidgetBottomSheetDialogImage.setImageResource( if (isLightModeEnabled) { - com.duckduckgo.app.browser.R.drawable.experiment_widget_promo_light + R.drawable.widget_promo_light } else { - com.duckduckgo.app.browser.R.drawable.experiment_widget_promo_dark + R.drawable.widget_promo_dark }, ) - binding.experimentHomeScreenWidgetBottomSheetDialogPrimaryButton.setOnClickListener { + binding.homeScreenWidgetBottomSheetDialogPrimaryButton.setOnClickListener { eventListener?.onAddWidgetButtonClicked() dismiss() } - binding.experimentHomeScreenWidgetBottomSheetDialogGhostButton.setOnClickListener { + binding.homeScreenWidgetBottomSheetDialogGhostButton.setOnClickListener { eventListener?.onNotNowButtonClicked() dismiss() } @@ -77,7 +77,7 @@ class ExperimentalHomeScreenWidgetBottomSheetDialog( */ private fun setRoundCorners(dialogInterface: DialogInterface) { val bottomSheetDialog = dialogInterface as BottomSheetDialog - val bottomSheet = bottomSheetDialog.findViewById(R.id.design_bottom_sheet) + val bottomSheet = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) val shapeDrawable = MaterialShapeDrawable.createWithElevationOverlay(context) shapeDrawable.shapeAppearanceModel = shapeDrawable.shapeAppearanceModel diff --git a/app/src/main/java/com/duckduckgo/app/browser/ui/dialogs/widgetprompt/OnboardingHomeScreenWidgetToggles.kt b/app/src/main/java/com/duckduckgo/app/browser/ui/dialogs/widgetprompt/OnboardingHomeScreenWidgetToggles.kt new file mode 100644 index 000000000000..12f49b15a1d4 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/ui/dialogs/widgetprompt/OnboardingHomeScreenWidgetToggles.kt @@ -0,0 +1,34 @@ +/* + * 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.browser.ui.dialogs.widgetprompt + +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle + +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "onboardingHomeScreenWidget", +) +interface OnboardingHomeScreenWidgetToggles { + + @Toggle.DefaultValue(Toggle.DefaultFeatureValue.FALSE) + fun self(): Toggle + + @Toggle.DefaultValue(Toggle.DefaultFeatureValue.FALSE) + fun onboardingHomeScreenWidgetPrompt(): Toggle +} 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 0594d19a9a8a..1187e5bb8089 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 @@ -21,7 +21,7 @@ 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.browser.defaultbrowsing.prompts.ui.experiment.OnboardingHomeScreenWidgetExperiment +import com.duckduckgo.app.browser.ui.dialogs.widgetprompt.OnboardingHomeScreenWidgetToggles import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.model.DismissedCta @@ -85,7 +85,7 @@ class CtaViewModel @Inject constructor( private val subscriptions: Subscriptions, private val duckPlayer: DuckPlayer, private val brokenSitePrompt: BrokenSitePrompt, - private val onboardingHomeScreenWidgetExperiment: OnboardingHomeScreenWidgetExperiment, + private val onboardingHomeScreenWidgetToggles: OnboardingHomeScreenWidgetToggles, private val onboardingDesignExperimentManager: OnboardingDesignExperimentManager, private val rebrandingFeatureToggle: SubscriptionRebrandingFeatureToggle, ) { @@ -169,9 +169,6 @@ class CtaViewModel @Inject constructor( } cta.cancelPixel?.let { - if (cta is AddWidgetAuto || cta is AddWidgetAutoOnboardingExperiment) { - onboardingHomeScreenWidgetExperiment.fireOnboardingWidgetDismiss() - } pixel.fire(it, cta.pixelCancelParameters()) } if (viaCloseBtn) { @@ -188,9 +185,6 @@ class CtaViewModel @Inject constructor( suspend fun onUserClickCtaOkButton(cta: Cta) { cta.okPixel?.let { - if (cta is AddWidgetAuto || cta is AddWidgetAutoOnboardingExperiment) { - onboardingHomeScreenWidgetExperiment.fireOnboardingWidgetAdd() - } pixel.fire(it, cta.pixelOkParameters()) } if (cta is BrokenSitePromptDialogCta) { @@ -285,12 +279,11 @@ class CtaViewModel @Inject constructor( // Add Widget canShowWidgetCta() -> { if (widgetCapabilities.supportsAutomaticWidgetAdd) { - onboardingHomeScreenWidgetExperiment.enroll() - if (onboardingHomeScreenWidgetExperiment.isOnboardingHomeScreenWidgetExperiment()) { - onboardingHomeScreenWidgetExperiment.fireOnboardingWidgetDisplay() + val showOnboardingHomeScreenWidgetPrompt = onboardingHomeScreenWidgetToggles.self().isEnabled() && + onboardingHomeScreenWidgetToggles.onboardingHomeScreenWidgetPrompt().isEnabled() + if (showOnboardingHomeScreenWidgetPrompt) { AddWidgetAutoOnboardingExperiment } else { - onboardingHomeScreenWidgetExperiment.fireOnboardingWidgetDisplay() AddWidgetAuto } } else { diff --git a/app/src/main/java/com/duckduckgo/app/settings/PostCtaExperienceToggles.kt b/app/src/main/java/com/duckduckgo/app/settings/PostCtaExperienceToggles.kt new file mode 100644 index 000000000000..9edf86fa8d9f --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/settings/PostCtaExperienceToggles.kt @@ -0,0 +1,34 @@ +/* + * 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.settings + +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle + +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "postCtaExperience", +) +interface PostCtaExperienceToggles { + + @Toggle.DefaultValue(Toggle.DefaultFeatureValue.FALSE) + fun self(): Toggle + + @Toggle.DefaultValue(Toggle.DefaultFeatureValue.FALSE) + fun simpleSearchWidgetPrompt(): Toggle +} diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt index 9dd4575be7dc..a1c3d833391c 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt @@ -64,7 +64,6 @@ import com.duckduckgo.app.settings.SettingsViewModel.Command.LaunchPrivateSearch import com.duckduckgo.app.settings.SettingsViewModel.Command.LaunchSyncSettings import com.duckduckgo.app.settings.SettingsViewModel.Command.LaunchWebTrackingProtectionScreen import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.app.widget.experiment.PostCtaExperienceExperiment import com.duckduckgo.app.widget.ui.WidgetCapabilities import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.autofill.api.AutofillCapabilityChecker @@ -124,7 +123,7 @@ class SettingsViewModel @Inject constructor( private val androidBrowserConfigFeature: AndroidBrowserConfigFeature, private val settingsPageFeature: SettingsPageFeature, private val widgetCapabilities: WidgetCapabilities, - private val postCtaExperienceExperiment: PostCtaExperienceExperiment, + private val postCtaExperienceToggles: PostCtaExperienceToggles, private val rebrandingFeatureToggle: SubscriptionRebrandingFeatureToggle, ) : ViewModel(), DefaultLifecycleObserver { @@ -255,13 +254,11 @@ class SettingsViewModel @Inject constructor( fun userRequestedToAddHomeScreenWidget() { viewModelScope.launch(dispatcherProvider.io()) { - postCtaExperienceExperiment.enroll() - val simpleWidgetPrompt = postCtaExperienceExperiment.isSimpleSearchWidgetPrompt() + val simpleWidgetPrompt = postCtaExperienceToggles.self().isEnabled() && postCtaExperienceToggles.simpleSearchWidgetPrompt().isEnabled() command.send(LaunchAddHomeScreenWidget(simpleWidgetPrompt)) if (!currentViewState().widgetsInstalled) { widgetPromptShown = true } - postCtaExperienceExperiment.fireSettingsWidgetDisplay() } } @@ -356,7 +353,6 @@ class SettingsViewModel @Inject constructor( widgetPromptShown = false if (!widgetsInstalled) { logcat { "Widget bottom sheet was dismissed." } - postCtaExperienceExperiment.fireSettingsWidgetDismiss() } } } diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index 0fb1d65d4cf2..31c4c9e643f5 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel -import com.duckduckgo.app.browser.defaultbrowsing.prompts.ui.experiment.OnboardingHomeScreenWidgetExperiment import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.onboarding.store.AppStage @@ -32,7 +31,6 @@ import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.UpdateVoiceSearch -import com.duckduckgo.app.widget.experiment.PostCtaExperienceExperiment import com.duckduckgo.browser.api.autocomplete.AutoComplete import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteResult import com.duckduckgo.browser.api.autocomplete.AutoComplete.AutoCompleteSuggestion @@ -91,8 +89,6 @@ class SystemSearchViewModel @Inject constructor( private val history: NavigationHistory, private val dispatchers: DispatcherProvider, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, - private val postCtaExperienceExperiment: PostCtaExperienceExperiment, - private val onboardingHomeScreenWidgetExperiment: OnboardingHomeScreenWidgetExperiment, ) : ViewModel(), EditSavedSiteDialogFragment.EditSavedSiteListener { data class OnboardingViewState( @@ -308,10 +304,6 @@ class SystemSearchViewModel @Inject constructor( userStageStore.stageCompleted(AppStage.NEW) command.value = Command.LaunchBrowser(query.trim()) pixel.fire(INTERSTITIAL_LAUNCH_BROWSER_QUERY) - postCtaExperienceExperiment.fireWidgetSearch() - postCtaExperienceExperiment.fireWidgetSearchXCount() - onboardingHomeScreenWidgetExperiment.fireWidgetSearch() - onboardingHomeScreenWidgetExperiment.fireWidgetSearchXCount() } } @@ -325,12 +317,6 @@ class SystemSearchViewModel @Inject constructor( } } pixel.fire(INTERSTITIAL_LAUNCH_BROWSER_QUERY) - viewModelScope.launch { - postCtaExperienceExperiment.fireWidgetSearch() - postCtaExperienceExperiment.fireWidgetSearchXCount() - onboardingHomeScreenWidgetExperiment.fireWidgetSearch() - onboardingHomeScreenWidgetExperiment.fireWidgetSearchXCount() - } } fun userLongPressedAutocomplete(suggestion: AutoCompleteSuggestion) { diff --git a/app/src/main/java/com/duckduckgo/app/widget/experiment/PostCtaExperienceExperiment.kt b/app/src/main/java/com/duckduckgo/app/widget/experiment/PostCtaExperienceExperiment.kt deleted file mode 100644 index 06f549c310dd..000000000000 --- a/app/src/main/java/com/duckduckgo/app/widget/experiment/PostCtaExperienceExperiment.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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.widget.experiment - -import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.app.widget.experiment.PostCtaExperienceToggles.Cohorts.CONTROL -import com.duckduckgo.app.widget.experiment.PostCtaExperienceToggles.Cohorts.VARIANT_SIMPLE_SEARCH_WIDGET_PROMPT -import com.duckduckgo.app.widget.experiment.store.WidgetSearchCountDataStore -import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.feature.toggles.api.MetricsPixel -import com.duckduckgo.feature.toggles.api.PixelDefinition -import com.squareup.anvil.annotations.ContributesBinding -import dagger.SingleInstanceIn -import java.time.LocalDate -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject -import kotlinx.coroutines.withContext - -interface PostCtaExperienceExperiment { - suspend fun enroll() - suspend fun isControl(): Boolean - suspend fun isSimpleSearchWidgetPrompt(): Boolean - - suspend fun fireSettingsWidgetDisplay() - suspend fun fireSettingsWidgetAdd() - - suspend fun fireSettingsWidgetDismiss() - suspend fun fireWidgetSearch() - suspend fun fireWidgetSearchXCount() -} - -@ContributesBinding( - scope = AppScope::class, - boundType = PostCtaExperienceExperiment::class, -) -@SingleInstanceIn(AppScope::class) -class PostCtaExperienceExperimentImpl @Inject constructor( - private val dispatcherProvider: DispatcherProvider, - private val postCtaExperienceToggles: PostCtaExperienceToggles, - private val postCtaExperiencePixelsPlugin: PostCtaExperiencePixelsPlugin, - private val pixel: Pixel, - private val widgetSearchCountDataStore: WidgetSearchCountDataStore, -) : PostCtaExperienceExperiment { - - override suspend fun enroll() { - postCtaExperienceToggles.postCtaExperienceExperimentJun25().enroll() - } - - override suspend fun isControl(): Boolean = - postCtaExperienceToggles.postCtaExperienceExperimentJun25().isEnrolledAndEnabled(CONTROL) - - override suspend fun isSimpleSearchWidgetPrompt(): Boolean = - postCtaExperienceToggles.postCtaExperienceExperimentJun25().isEnrolledAndEnabled(VARIANT_SIMPLE_SEARCH_WIDGET_PROMPT) - - override suspend fun fireSettingsWidgetDisplay() { - withContext(dispatcherProvider.io()) { - postCtaExperiencePixelsPlugin.getSettingsWidgetDisplayMetric()?.fire() - } - } - - override suspend fun fireSettingsWidgetAdd() { - withContext(dispatcherProvider.io()) { - postCtaExperiencePixelsPlugin.getSettingsWidgetAddMetric()?.fire() - } - } - - override suspend fun fireSettingsWidgetDismiss() { - withContext(dispatcherProvider.io()) { - postCtaExperiencePixelsPlugin.getSettingsWidgetDismissMetric()?.fire() - } - } - - override suspend fun fireWidgetSearch() { - withContext(dispatcherProvider.io()) { - postCtaExperiencePixelsPlugin.getWidgetSearchMetric()?.fire() - } - } - - override suspend fun fireWidgetSearchXCount() { - withContext(dispatcherProvider.io()) { - postCtaExperiencePixelsPlugin.getWidgetSearch3xMetric()?.getPixelDefinitions()?.forEach { definition -> - if (isInConversionWindow(definition)) { - widgetSearchCountDataStore.getMetricForPixelDefinition(definition).takeIf { it < 3 }?.let { - widgetSearchCountDataStore.increaseMetricForPixelDefinition(definition).takeIf { it == 3 }?.apply { - pixel.fire(definition.pixelName, definition.params) - } - } - } - } - - postCtaExperiencePixelsPlugin.getWidgetSearch5xMetric()?.getPixelDefinitions()?.forEach { definition -> - if (isInConversionWindow(definition)) { - widgetSearchCountDataStore.getMetricForPixelDefinition(definition).takeIf { it < 5 }?.let { - widgetSearchCountDataStore.increaseMetricForPixelDefinition(definition).takeIf { it == 5 }?.apply { - pixel.fire(definition.pixelName, definition.params) - } - } - } - } - } - } - - private fun isInConversionWindow(definition: PixelDefinition): Boolean { - val enrollmentDate = definition.params["enrollmentDate"] ?: return false - val lowerWindow = definition.params["conversionWindowDays"]?.split("-")?.first()?.toInt() ?: return false - val upperWindow = definition.params["conversionWindowDays"]?.split("-")?.last()?.toInt() ?: return false - val daysDiff = daysBetweenTodayAnd(enrollmentDate) - - return (daysDiff in lowerWindow..upperWindow) - } - - private fun daysBetweenTodayAnd(date: String): Long { - val today = ZonedDateTime.now(ZoneId.of("America/New_York")) - val localDate = LocalDate.parse(date) - val zoneDateTime: ZonedDateTime = localDate.atStartOfDay(ZoneId.of("America/New_York")) - return ChronoUnit.DAYS.between(zoneDateTime, today) - } - - private fun MetricsPixel.fire() = getPixelDefinitions().forEach { - pixel.fire(it.pixelName, it.params) - } -} diff --git a/app/src/main/java/com/duckduckgo/app/widget/experiment/PostCtaExperienceToggles.kt b/app/src/main/java/com/duckduckgo/app/widget/experiment/PostCtaExperienceToggles.kt deleted file mode 100644 index 34e098751e7c..000000000000 --- a/app/src/main/java/com/duckduckgo/app/widget/experiment/PostCtaExperienceToggles.kt +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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.widget.experiment - -import com.duckduckgo.anvil.annotations.ContributesRemoteFeature -import com.duckduckgo.app.widget.experiment.PostCtaExperienceToggles.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.State.CohortName -import com.squareup.anvil.annotations.ContributesMultibinding -import dagger.SingleInstanceIn -import javax.inject.Inject - -@ContributesRemoteFeature( - scope = AppScope::class, - featureName = BASE_EXPERIMENT_NAME, -) -interface PostCtaExperienceToggles { - - @Toggle.DefaultValue(Toggle.DefaultFeatureValue.FALSE) - fun self(): Toggle - - @Toggle.DefaultValue(Toggle.DefaultFeatureValue.FALSE) - fun postCtaExperienceExperimentJun25(): Toggle - - enum class Cohorts(override val cohortName: String) : CohortName { - CONTROL("control"), // Search and Favorites widget prompt - VARIANT_SIMPLE_SEARCH_WIDGET_PROMPT("simpleSearchWidgetPrompt"), // Simple Search widget prompt - } - - companion object { - internal const val BASE_EXPERIMENT_NAME = "postCtaExperience" - } -} - -@ContributesMultibinding(AppScope::class) -@SingleInstanceIn(AppScope::class) -class PostCtaExperiencePixelsPlugin @Inject constructor( - private val toggles: PostCtaExperienceToggles, -) : MetricsPixelPlugin { - - override suspend fun getMetrics(): List { - return listOf( - MetricsPixel( - metric = METRIC_SETTINGS_WIDGET_DISPLAY, - value = "1", - toggle = toggles.postCtaExperienceExperimentJun25(), - conversionWindow = listOf( - ConversionWindow(lowerWindow = 0, upperWindow = 0), - ), - ), - MetricsPixel( - metric = METRIC_SETTINGS_WIDGET_ADD, - value = "1", - toggle = toggles.postCtaExperienceExperimentJun25(), - conversionWindow = listOf( - ConversionWindow(lowerWindow = 0, upperWindow = 0), - ), - ), - MetricsPixel( - metric = METRIC_SETTINGS_WIDGET_DISMISS, - value = "1", - toggle = toggles.postCtaExperienceExperimentJun25(), - conversionWindow = listOf( - ConversionWindow(lowerWindow = 0, upperWindow = 0), - ), - ), - MetricsPixel( - metric = METRIC_WIDGET_SEARCH, - value = "1", - toggle = toggles.postCtaExperienceExperimentJun25(), - conversionWindow = listOf( - ConversionWindow(lowerWindow = 5, upperWindow = 7), - ConversionWindow(lowerWindow = 8, upperWindow = 14), - ), - ), - MetricsPixel( - metric = METRIC_WIDGET_SEARCH_3X, - value = "1", - toggle = toggles.postCtaExperienceExperimentJun25(), - conversionWindow = listOf( - ConversionWindow(lowerWindow = 5, upperWindow = 7), - ), - ), - MetricsPixel( - metric = METRIC_WIDGET_SEARCH_5X, - value = "1", - toggle = toggles.postCtaExperienceExperimentJun25(), - conversionWindow = listOf( - ConversionWindow(lowerWindow = 5, upperWindow = 7), - ), - ), - ) - } - - suspend fun getSettingsWidgetDisplayMetric(): MetricsPixel? { - return this.getMetrics().firstOrNull { it.metric == METRIC_SETTINGS_WIDGET_DISPLAY } - } - - suspend fun getSettingsWidgetAddMetric(): MetricsPixel? { - return this.getMetrics().firstOrNull { it.metric == METRIC_SETTINGS_WIDGET_ADD } - } - - suspend fun getSettingsWidgetDismissMetric(): MetricsPixel? { - return this.getMetrics().firstOrNull { it.metric == METRIC_SETTINGS_WIDGET_DISMISS } - } - - suspend fun getWidgetSearchMetric(): MetricsPixel? { - return this.getMetrics().firstOrNull { it.metric == METRIC_WIDGET_SEARCH } - } - - suspend fun getWidgetSearch3xMetric(): MetricsPixel? { - return this.getMetrics().firstOrNull { it.metric == METRIC_WIDGET_SEARCH_3X } - } - - suspend fun getWidgetSearch5xMetric(): MetricsPixel? { - return this.getMetrics().firstOrNull { it.metric == METRIC_WIDGET_SEARCH_5X } - } - - companion object { - internal const val METRIC_SETTINGS_WIDGET_DISPLAY = "settingsWidgetDisplay" - internal const val METRIC_SETTINGS_WIDGET_ADD = "settingsWidgetAdd" - internal const val METRIC_SETTINGS_WIDGET_DISMISS = "settingsWidgetDismiss" - internal const val METRIC_WIDGET_SEARCH = "widgetSearch" - internal const val METRIC_WIDGET_SEARCH_3X = "widgetSearch3x" - internal const val METRIC_WIDGET_SEARCH_5X = "widgetSearch5x" - } -} diff --git a/app/src/main/java/com/duckduckgo/app/widget/experiment/di/WidgetSearchCountDataStoreModule.kt b/app/src/main/java/com/duckduckgo/app/widget/experiment/di/WidgetSearchCountDataStoreModule.kt deleted file mode 100644 index 4c51534e2f1b..000000000000 --- a/app/src/main/java/com/duckduckgo/app/widget/experiment/di/WidgetSearchCountDataStoreModule.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.widget.experiment.di - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.preferencesDataStore -import com.duckduckgo.di.scopes.AppScope -import com.squareup.anvil.annotations.ContributesTo -import dagger.Module -import dagger.Provides -import javax.inject.Qualifier - -@ContributesTo(AppScope::class) -@Module -object WidgetSearchCountDataStoreModule { - private val Context.widgetSearchCountDataStore: DataStore by preferencesDataStore( - name = "widget_search_count", - ) - - @Provides - @WidgetSearchCount - fun provideWidgetSearchCountDataStore(context: Context): DataStore = context.widgetSearchCountDataStore -} - -@Qualifier -internal annotation class WidgetSearchCount diff --git a/app/src/main/java/com/duckduckgo/app/widget/experiment/store/WidgetSearchCountDataStore.kt b/app/src/main/java/com/duckduckgo/app/widget/experiment/store/WidgetSearchCountDataStore.kt deleted file mode 100644 index 452d7289ae48..000000000000 --- a/app/src/main/java/com/duckduckgo/app/widget/experiment/store/WidgetSearchCountDataStore.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.widget.experiment.store - -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.intPreferencesKey -import com.duckduckgo.app.widget.experiment.di.WidgetSearchCount -import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.feature.toggles.api.PixelDefinition -import com.squareup.anvil.annotations.ContributesBinding -import dagger.SingleInstanceIn -import javax.inject.Inject -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map - -interface WidgetSearchCountDataStore { - suspend fun increaseMetricForPixelDefinition(definition: PixelDefinition): Int - suspend fun getMetricForPixelDefinition(definition: PixelDefinition): Int -} - -@ContributesBinding(AppScope::class) -@SingleInstanceIn(AppScope::class) -class SharedPreferencesWidgetSearchCountDataStore @Inject constructor( - @WidgetSearchCount private val store: DataStore, -) : WidgetSearchCountDataStore { - - override suspend fun increaseMetricForPixelDefinition(definition: PixelDefinition): Int { - val tag = "$definition" - val currentCount = getMetricForPixelDefinition(definition) - store.edit { preferences -> - preferences[intPreferencesKey(tag)] = currentCount + 1 - } - return currentCount + 1 - } - - override suspend fun getMetricForPixelDefinition(definition: PixelDefinition): Int { - val tag = "$definition" - return store.data.map { preferences -> - preferences[intPreferencesKey(tag)] ?: 0 - }.firstOrNull() ?: 0 - } -} diff --git a/app/src/main/java/com/duckduckgo/widget/SearchWidgetLifecycleDelegate.kt b/app/src/main/java/com/duckduckgo/widget/SearchWidgetLifecycleDelegate.kt index 8ac6c4f426ec..0d1d3a8a26d2 100644 --- a/app/src/main/java/com/duckduckgo/widget/SearchWidgetLifecycleDelegate.kt +++ b/app/src/main/java/com/duckduckgo/widget/SearchWidgetLifecycleDelegate.kt @@ -20,7 +20,6 @@ import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.AppPixelName.WIDGETS_DELETED import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.app.widget.experiment.PostCtaExperienceExperiment import com.duckduckgo.app.widget.ui.AppWidgetCapabilities import javax.inject.Inject @@ -28,16 +27,14 @@ class SearchWidgetLifecycleDelegate @Inject constructor( private val appInstallStore: AppInstallStore, private val widgetCapabilities: AppWidgetCapabilities, private val pixel: Pixel, - private val postCtaExperienceExperiment: PostCtaExperienceExperiment, ) { - suspend fun handleOnWidgetEnabled(widgetSpecificAddedPixel: AppPixelName) { + fun handleOnWidgetEnabled(widgetSpecificAddedPixel: AppPixelName) { if (!appInstallStore.widgetInstalled) { appInstallStore.widgetInstalled = true pixel.fire(AppPixelName.WIDGETS_ADDED) } pixel.fire(widgetSpecificAddedPixel) - postCtaExperienceExperiment.fireSettingsWidgetAdd() } fun handleOnWidgetDisabled(widgetSpecificDeletedPixel: AppPixelName) { diff --git a/app/src/main/res/drawable/experiment_widget_promo_dark.xml b/app/src/main/res/drawable/widget_promo_dark.xml similarity index 100% rename from app/src/main/res/drawable/experiment_widget_promo_dark.xml rename to app/src/main/res/drawable/widget_promo_dark.xml diff --git a/app/src/main/res/drawable/experiment_widget_promo_light.xml b/app/src/main/res/drawable/widget_promo_light.xml similarity index 100% rename from app/src/main/res/drawable/experiment_widget_promo_light.xml rename to app/src/main/res/drawable/widget_promo_light.xml diff --git a/app/src/main/res/layout/bottom_sheet_experiment_home_screen_widget.xml b/app/src/main/res/layout/bottom_sheet_home_screen_widget.xml similarity index 93% rename from app/src/main/res/layout/bottom_sheet_experiment_home_screen_widget.xml rename to app/src/main/res/layout/bottom_sheet_home_screen_widget.xml index 666198931805..5ab1fb88c3f9 100644 --- a/app/src/main/res/layout/bottom_sheet_experiment_home_screen_widget.xml +++ b/app/src/main/res/layout/bottom_sheet_home_screen_widget.xml @@ -40,11 +40,11 @@ tools:ignore="DeprecatedWidgetInXml">