From 752374851673257cdaf28e8db8cc92801d145f3c Mon Sep 17 00:00:00 2001 From: joshliebe Date: Tue, 25 Nov 2025 09:15:14 +0000 Subject: [PATCH 1/2] Add Input Screen onboarding step --- .../NewAddressBarOptionManager.kt | 10 +- .../OnboardingInputScreenSelectionObserver.kt | 58 ++++++++++ .../app/onboarding/store/OnboardingStore.kt | 2 + .../onboarding/store/OnboardingStoreImpl.kt | 13 +++ .../app/onboarding/ui/page/BbWelcomePage.kt | 7 ++ .../app/onboarding/ui/page/BuckWelcomePage.kt | 7 ++ .../ui/page/PreOnboardingDialogType.kt | 1 + .../app/onboarding/ui/page/WelcomePage.kt | 90 ++++++++++++++- .../ui/page/WelcomePageViewModel.kt | 24 ++++ .../layout/pre_onboarding_dax_dialog_cta.xml | 103 ++++++++++++++++++ app/src/main/res/values/strings.xml | 3 + .../com/duckduckgo/duckchat/api/DuckChat.kt | 5 + .../duckduckgo/duckchat/impl/RealDuckChat.kt | 5 - 13 files changed, 320 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/onboarding/OnboardingInputScreenSelectionObserver.kt diff --git a/app/src/main/java/com/duckduckgo/app/browser/newaddressbaroption/NewAddressBarOptionManager.kt b/app/src/main/java/com/duckduckgo/app/browser/newaddressbaroption/NewAddressBarOptionManager.kt index b6aa5a46d6fc..d7e2f58fa7a0 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newaddressbaroption/NewAddressBarOptionManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newaddressbaroption/NewAddressBarOptionManager.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.browser.newaddressbaroption import android.app.Activity import com.duckduckgo.app.browser.omnibar.OmnibarType 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.settings.db.SettingsDataStore import com.duckduckgo.common.ui.DuckDuckGoActivity @@ -55,6 +56,7 @@ class RealNewAddressBarOptionManager @Inject constructor( private val remoteMessagingRepository: RemoteMessagingRepository, private val newAddressBarOptionDataStore: NewAddressBarOptionDataStore, private val settingsDataStore: SettingsDataStore, + private val onboardingStore: OnboardingStore, private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(), ) : NewAddressBarOptionManager { private val showChoiceScreenMutex = Mutex() @@ -97,7 +99,8 @@ class RealNewAddressBarOptionManager @Inject constructor( hasNotInteractedWithSearchAndDuckAiRMF() && isNewAddressBarOptionChoiceScreenEnabled() && isNotLaunchedFromExternal(isLaunchedFromExternal) && - isSubsequentLaunch() + isSubsequentLaunch() && + hasNoInputScreenSelection() } private fun isActivityValid(activity: Activity): Boolean = @@ -130,6 +133,11 @@ class RealNewAddressBarOptionManager @Inject constructor( logcat(DEBUG) { "NewAddressBarOptionManager: $it isInputScreenDisabled" } } + private fun hasNoInputScreenSelection(): Boolean = + (onboardingStore.getInputScreenSelection() == null).also { + logcat(DEBUG) { "NewAddressBarOptionManager: $it hasNoInputScreenSelection" } + } + private fun isBottomAddressBarDisabled(): Boolean = (settingsDataStore.omnibarType != OmnibarType.SINGLE_BOTTOM).also { logcat(DEBUG) { "NewAddressBarOptionManager: $it isBottomAddressBarDisabled" } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/OnboardingInputScreenSelectionObserver.kt b/app/src/main/java/com/duckduckgo/app/onboarding/OnboardingInputScreenSelectionObserver.kt new file mode 100644 index 000000000000..9901ac3adafb --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/onboarding/OnboardingInputScreenSelectionObserver.kt @@ -0,0 +1,58 @@ +/* + * 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.onboarding + +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver +import com.duckduckgo.app.onboarding.store.AppStage +import com.duckduckgo.app.onboarding.store.OnboardingStore +import com.duckduckgo.app.onboarding.store.UserStageStore +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.duckchat.api.DuckChat +import com.squareup.anvil.annotations.ContributesMultibinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ContributesMultibinding( + scope = AppScope::class, + boundType = MainProcessLifecycleObserver::class, +) +class OnboardingInputScreenSelectionObserver @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + dispatchers: DispatcherProvider, + private val userStageStore: UserStageStore, + private val onboardingStore: OnboardingStore, + private val duckChat: DuckChat, +) : MainProcessLifecycleObserver { + + init { + appCoroutineScope.launch(dispatchers.io()) { + userStageStore.userAppStageFlow() + .distinctUntilChanged() + .filter { it == AppStage.ESTABLISHED } + .collect { + onboardingStore.getInputScreenSelection()?.let { selection -> + duckChat.setInputScreenUserSetting(selection) + } + } + } + } +} 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 b461ef5a7541..3bf23addb746 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 @@ -23,4 +23,6 @@ interface OnboardingStore { fun getSearchOptions(): List fun getSitesOptions(): List + fun storeInputScreenSelection(selected: Boolean) + fun getInputScreenSelection(): Boolean? } 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 8433641fe226..e610313ecc82 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 @@ -163,8 +163,21 @@ class OnboardingStoreImpl @Inject constructor( ) } + override fun storeInputScreenSelection(selected: Boolean) { + preferences.edit { putBoolean(KEY_INPUT_SCREEN_SELECTION, selected) } + } + + override fun getInputScreenSelection(): Boolean? { + return if (preferences.contains(KEY_INPUT_SCREEN_SELECTION)) { + preferences.getBoolean(KEY_INPUT_SCREEN_SELECTION, false) + } else { + null + } + } + companion object { const val FILENAME = "com.duckduckgo.app.onboarding.settings" const val ONBOARDING_JOURNEY = "onboardingJourney" + private const val KEY_INPUT_SCREEN_SELECTION = "inputScreenSelection" } } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BbWelcomePage.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BbWelcomePage.kt index d3edda420799..623d178f3604 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BbWelcomePage.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BbWelcomePage.kt @@ -57,8 +57,10 @@ import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.ADDRESS_BAR import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.COMPARISON_CHART import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INITIAL import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INITIAL_REINSTALL_USER +import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INPUT_SCREEN import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.SKIP_ONBOARDING_OPTION import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.* +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowInputScreenDialog import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.ui.store.AppTheme import com.duckduckgo.common.ui.view.TypeAnimationTextView @@ -116,6 +118,7 @@ class BbWelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome is ShowSkipOnboardingOption -> configureDaxCta(SKIP_ONBOARDING_OPTION) is ShowDefaultBrowserDialog -> showDefaultBrowserDialog(it.intent) is ShowAddressBarPositionDialog -> configureDaxCta(ADDRESS_BAR_POSITION) + is ShowInputScreenDialog -> onContinuePressed() is Finish -> onContinuePressed() is OnboardingSkipped -> onSkipPressed() is SetAddressBarPositionOptions -> setAddressBarPositionOptions(it.defaultOption) @@ -436,6 +439,10 @@ class BbWelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome scheduleTypingAnimation(binding.daxDialogCta.addressBarPosition.dialogTitle, titleText) { afterTypingAnimation() } backgroundSceneManager?.transitionToNextTile(expectedTile = TILE_04) } + + INPUT_SCREEN -> { + // Ignored + } } backgroundSceneManager?.setBackgroundClickListener(afterTypingAnimation) binding.daxDialogCta.cardContainer.setOnClickListener { afterTypingAnimation() } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BuckWelcomePage.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BuckWelcomePage.kt index 2163af97e812..5bc2932d6685 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BuckWelcomePage.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BuckWelcomePage.kt @@ -53,6 +53,7 @@ import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.ADDRESS_BAR import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.COMPARISON_CHART import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INITIAL import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INITIAL_REINSTALL_USER +import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INPUT_SCREEN import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.SKIP_ONBOARDING_OPTION import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.Finish import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.OnboardingSkipped @@ -62,6 +63,7 @@ import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowCo import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowDefaultBrowserDialog import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowInitialDialog import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowInitialReinstallUserDialog +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowInputScreenDialog import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowSkipOnboardingOption import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.ui.store.AppTheme @@ -116,6 +118,7 @@ class BuckWelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welco is ShowSkipOnboardingOption -> configureDaxCta(SKIP_ONBOARDING_OPTION) is ShowDefaultBrowserDialog -> showDefaultBrowserDialog(it.intent) is ShowAddressBarPositionDialog -> configureDaxCta(ADDRESS_BAR_POSITION) + is ShowInputScreenDialog -> onContinuePressed() is Finish -> onContinuePressed() is OnboardingSkipped -> onSkipPressed() is SetAddressBarPositionOptions -> setAddressBarPositionOptions(it.defaultOption) @@ -395,6 +398,10 @@ class BuckWelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welco }, ) } + + INPUT_SCREEN -> { + // Ignored + } } binding.sceneBg.setOnClickListener { afterTypingAnimation() } binding.daxDialogCta.cardContainer.setOnClickListener { afterTypingAnimation() } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/PreOnboardingDialogType.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/PreOnboardingDialogType.kt index d99b638c1351..e2d57d170af6 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/PreOnboardingDialogType.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/PreOnboardingDialogType.kt @@ -22,4 +22,5 @@ enum class PreOnboardingDialogType { SKIP_ONBOARDING_OPTION, COMPARISON_CHART, ADDRESS_BAR_POSITION, + INPUT_SCREEN, } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt index 48c9ff98e853..74687ff088a0 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt @@ -41,6 +41,7 @@ import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.ADDRESS_BAR import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.COMPARISON_CHART import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INITIAL import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INITIAL_REINSTALL_USER +import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INPUT_SCREEN import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.SKIP_ONBOARDING_OPTION import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.Finish import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.OnboardingSkipped @@ -50,6 +51,7 @@ import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowCo import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowDefaultBrowserDialog import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowInitialDialog import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowInitialReinstallUserDialog +import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowInputScreenDialog import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowSkipOnboardingOption import com.duckduckgo.app.onboardingdesignexperiment.OnboardingDesignExperimentManager import com.duckduckgo.appbuildconfig.api.AppBuildConfig @@ -64,6 +66,7 @@ import com.duckduckgo.di.scopes.FragmentScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import javax.inject.Inject +import com.duckduckgo.mobile.android.R as CommonR @InjectWith(FragmentScope::class) class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_page) { @@ -110,6 +113,7 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p is ShowSkipOnboardingOption -> configureDaxCta(SKIP_ONBOARDING_OPTION) is ShowDefaultBrowserDialog -> showDefaultBrowserDialog(it.intent) is ShowAddressBarPositionDialog -> configureDaxCta(ADDRESS_BAR_POSITION) + is ShowInputScreenDialog -> configureDaxCta(INPUT_SCREEN) is Finish -> onContinuePressed() is OnboardingSkipped -> onSkipPressed() is SetAddressBarPositionOptions -> setAddressBarPositionOptions(it.defaultOption) @@ -243,7 +247,7 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p binding.daxDialogCta.dialogTextCta.text = "" TransitionManager.beginDelayedTransition(binding.daxDialogCta.cardView, AutoTransition()) binding.daxDialogCta.progressBarText.show() - binding.daxDialogCta.progressBarText.text = "1 / 2" + binding.daxDialogCta.progressBarText.text = "1 / 3" binding.daxDialogCta.progressBar.show() binding.daxDialogCta.progressBar.progress = 1 val ctaText = it.getString(R.string.preOnboardingDaxDialog2Title) @@ -296,7 +300,7 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p binding.daxDialogCta.comparisonChart.root.gone() TransitionManager.beginDelayedTransition(binding.daxDialogCta.cardView, AutoTransition()) binding.daxDialogCta.progressBarText.show() - binding.daxDialogCta.progressBarText.text = "2 / 2" + binding.daxDialogCta.progressBarText.text = "2 / 3" binding.daxDialogCta.progressBar.show() binding.daxDialogCta.progressBar.progress = 2 val ctaText = it.getString(R.string.preOnboardingAddressBarTitle).run { @@ -328,6 +332,52 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p scheduleTypingAnimation(ctaText) { afterAnimation() } } + + INPUT_SCREEN -> { + binding.daxDialogCta.descriptionCta.gone() + binding.daxDialogCta.secondaryCta.gone() + binding.daxDialogCta.dialogTextCta.text = "" + binding.daxDialogCta.comparisonChart.root.gone() + binding.daxDialogCta.addressBarPosition.root.gone() + TransitionManager.beginDelayedTransition(binding.daxDialogCta.cardView, AutoTransition()) + binding.daxDialogCta.progressBarText.show() + binding.daxDialogCta.progressBarText.text = "3 / 3" + binding.daxDialogCta.progressBar.show() + binding.daxDialogCta.progressBar.progress = 3 + val ctaText = it.getString(R.string.preOnboardingAiChatAccessTitle) + binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) + binding.daxDialogCta.primaryCta.alpha = MIN_ALPHA + binding.daxDialogCta.duckAiInputScreenToggleContainer.show() + binding.daxDialogCta.duckAiInputScreenToggleContainer.alpha = MIN_ALPHA + + val isLightMode = appTheme.isLightModeEnabled() + updateAiChatToggleState(binding, isLightMode, withAi = true) + viewModel.onInputScreenOptionSelected(withAi = true) + + binding.daxDialogCta.duckAiInputScreenWithoutAiContainer.setOnClickListener { + updateAiChatToggleState(binding, isLightMode, withAi = false) + viewModel.onInputScreenOptionSelected(withAi = false) + } + binding.daxDialogCta.duckAiInputScreenWithAiContainer.setOnClickListener { + updateAiChatToggleState(binding, isLightMode, withAi = true) + viewModel.onInputScreenOptionSelected(withAi = true) + } + + val descriptionText = it.getString(R.string.preOnboardingAiChatAccessDescription) + binding.daxDialogCta.duckAiInputScreenToggleDescription.text = descriptionText.html(it) + binding.daxDialogCta.duckAiInputScreenToggleDescription.show() + binding.daxDialogCta.duckAiInputScreenToggleDescription.alpha = MIN_ALPHA + + afterAnimation = { + binding.daxDialogCta.dialogTextCta.finishAnimation() + binding.daxDialogCta.primaryCta.text = it.getString(R.string.preOnboardingAiChatAccessButton) + binding.daxDialogCta.primaryCta.setOnClickListener { viewModel.onPrimaryCtaClicked(INPUT_SCREEN) } + binding.daxDialogCta.primaryCta.animate().alpha(MAX_ALPHA).duration = ANIMATION_DURATION + binding.daxDialogCta.duckAiInputScreenToggleContainer.animate().alpha(MAX_ALPHA).duration = ANIMATION_DURATION + binding.daxDialogCta.duckAiInputScreenToggleDescription.animate().alpha(MAX_ALPHA).duration = ANIMATION_DURATION + } + scheduleTypingAnimation(ctaText) { afterAnimation() } + } } binding.sceneBg.setOnClickListener { afterAnimation() } binding.daxDialogCta.cardContainer.setOnClickListener { afterAnimation() } @@ -397,6 +447,42 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p binding.sceneBg.setImageResource(backgroundRes) } + private fun updateAiChatToggleState( + binding: ContentOnboardingWelcomePageBinding, + isLightMode: Boolean, + withAi: Boolean, + ) { + val withoutAiImageRes = when { + !withAi && isLightMode -> com.duckduckgo.duckchat.impl.R.drawable.searchbox_withoutai_active + !withAi && !isLightMode -> com.duckduckgo.duckchat.impl.R.drawable.searchbox_withoutai_active_dark + withAi && isLightMode -> com.duckduckgo.duckchat.impl.R.drawable.searchbox_withoutai_inactive + else -> com.duckduckgo.duckchat.impl.R.drawable.searchbox_withoutai_inactive_dark + } + val withAiImageRes = when { + withAi && isLightMode -> com.duckduckgo.duckchat.impl.R.drawable.searchbox_withai_active + withAi && !isLightMode -> com.duckduckgo.duckchat.impl.R.drawable.searchbox_withai_active_dark + !withAi && isLightMode -> com.duckduckgo.duckchat.impl.R.drawable.searchbox_withai_inactive + else -> com.duckduckgo.duckchat.impl.R.drawable.searchbox_withai_inactive_dark + } + + binding.daxDialogCta.duckAiInputScreenToggleWithoutAiImage.setImageResource(withoutAiImageRes) + binding.daxDialogCta.duckAiInputScreenToggleWithAiImage.setImageResource(withAiImageRes) + + val withoutAiCheckRes = if (!withAi) { + CommonR.drawable.ic_check_accent_24 + } else { + CommonR.drawable.ic_shape_circle_24 + } + val withAiCheckRes = if (withAi) { + CommonR.drawable.ic_check_accent_24 + } else { + CommonR.drawable.ic_shape_circle_24 + } + + binding.daxDialogCta.duckAiInputScreenToggleWithoutAiCheck.setImageResource(withoutAiCheckRes) + binding.daxDialogCta.duckAiInputScreenToggleWithAiCheck.setImageResource(withAiCheckRes) + } + companion object { private const val MIN_ALPHA = 0f private const val MAX_ALPHA = 1f diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModel.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModel.kt index 27e14fd1d33a..89ba9c975a97 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModel.kt @@ -25,10 +25,12 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.browser.omnibar.OmnibarType import com.duckduckgo.app.global.DefaultRoleBrowserDialog import com.duckduckgo.app.global.install.AppInstallStore +import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.ADDRESS_BAR_POSITION import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.COMPARISON_CHART import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INITIAL import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INITIAL_REINSTALL_USER +import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INPUT_SCREEN import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.SKIP_ONBOARDING_OPTION import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.Finish import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.OnboardingSkipped @@ -78,11 +80,13 @@ class WelcomePageViewModel @Inject constructor( private val dispatchers: DispatcherProvider, private val appBuildConfig: AppBuildConfig, private val onboardingDesignExperimentManager: OnboardingDesignExperimentManager, + private val onboardingStore: OnboardingStore, ) : ViewModel() { private val _commands = Channel(1, DROP_OLDEST) val commands: Flow = _commands.receiveAsFlow() private var defaultAddressBarPosition: Boolean = true + private var inputScreenSelected: Boolean = true sealed interface Command { data object ShowInitialReinstallUserDialog : Command @@ -99,6 +103,8 @@ class WelcomePageViewModel @Inject constructor( data object ShowAddressBarPositionDialog : Command + data object ShowInputScreenDialog : Command + data object Finish : Command data object OnboardingSkipped : Command @@ -163,6 +169,13 @@ class WelcomePageViewModel @Inject constructor( } else { onboardingDesignExperimentManager.fireAddressBarSetTopPixel() } + _commands.send(Command.ShowInputScreenDialog) + } + } + + INPUT_SCREEN -> { + viewModelScope.launch(dispatchers.io()) { + onboardingStore.storeInputScreenSelection(inputScreenSelected) _commands.send(Finish) } } @@ -196,6 +209,10 @@ class WelcomePageViewModel @Inject constructor( ADDRESS_BAR_POSITION -> { // no-op } + + INPUT_SCREEN -> { + // no-op + } } } @@ -255,6 +272,9 @@ class WelcomePageViewModel @Inject constructor( onboardingDesignExperimentManager.fireSetAddressBarDisplayedPixel() } } + INPUT_SCREEN -> { + // Pixel tracking can be added here if needed + } } } @@ -265,6 +285,10 @@ class WelcomePageViewModel @Inject constructor( } } + fun onInputScreenOptionSelected(withAi: Boolean) { + inputScreenSelected = withAi + } + fun loadDaxDialog() { viewModelScope.launch { if (isAppReinstall()) { diff --git a/app/src/main/res/layout/pre_onboarding_dax_dialog_cta.xml b/app/src/main/res/layout/pre_onboarding_dax_dialog_cta.xml index 20d96eeea166..3c5d32566af9 100644 --- a/app/src/main/res/layout/pre_onboarding_dax_dialog_cta.xml +++ b/app/src/main/res/layout/pre_onboarding_dax_dialog_cta.xml @@ -141,6 +141,109 @@ layout="@layout/pre_onboarding_address_bar_position" android:visibility="gone" /> + + + + + + + + + + + + + + + + + + + + + + + + + + Easy to see Bottom Easy to reach + Want easy access to private AI chat? + Next + Settings > AI Features.]]> Try a search! Your DuckDuckGo searches are always private. how to say "duck" in english diff --git a/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt b/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt index e4dc75d84b38..21772b638511 100644 --- a/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt +++ b/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt @@ -67,4 +67,9 @@ interface DuckChat { * Displays the new address bar option choice screen. */ fun showNewAddressBarOptionChoiceScreen(context: Context, isDarkThemeEnabled: Boolean) + + /** + * Set user setting to determine whether dedicated Duck.ai input screen with a mode switch should be used. + */ + suspend fun setInputScreenUserSetting(enabled: Boolean) } diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt index c66843ebbaf1..60d781a81e7e 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt @@ -73,11 +73,6 @@ interface DuckChatInternal : DuckChat { */ suspend fun setEnableDuckChatUserSetting(enabled: Boolean) - /** - * Set user setting to determine whether dedicated Duck.ai input screen with a mode switch should be used. - */ - suspend fun setInputScreenUserSetting(enabled: Boolean) - /** * Set user setting to determine whether DuckChat should be shown in browser menu. */ From fbb0bfb7496cd148550274b7c9e6c8ee9b987d40 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Tue, 25 Nov 2025 11:34:30 +0000 Subject: [PATCH 2/2] Add Input Screen onboarding step --- .../app/onboarding/ui/page/WelcomePage.kt | 18 ++- .../ui/page/WelcomePageViewModel.kt | 25 +++- .../AndroidBrowserConfigFeature.kt | 8 ++ app/src/main/res/values/donottranslate.xml | 5 + app/src/main/res/values/strings.xml | 3 - .../NewAddressBarOptionManagerTest.kt | 28 ++++ ...oardingInputScreenSelectionObserverTest.kt | 123 ++++++++++++++++++ .../ui/page/WelcomePageViewModelTest.kt | 71 ++++++++++ .../impl/messaging/fakes/FakeDuckChat.kt | 4 + 9 files changed, 274 insertions(+), 11 deletions(-) create mode 100644 app/src/test/java/com/duckduckgo/app/onboarding/OnboardingInputScreenSelectionObserverTest.kt diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt index 74687ff088a0..a7c62baeb0e9 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt @@ -247,8 +247,10 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p binding.daxDialogCta.dialogTextCta.text = "" TransitionManager.beginDelayedTransition(binding.daxDialogCta.cardView, AutoTransition()) binding.daxDialogCta.progressBarText.show() - binding.daxDialogCta.progressBarText.text = "1 / 3" + val maxPages = viewModel.getMaxPageCount() + binding.daxDialogCta.progressBarText.text = "1 / $maxPages" binding.daxDialogCta.progressBar.show() + binding.daxDialogCta.progressBar.max = maxPages binding.daxDialogCta.progressBar.progress = 1 val ctaText = it.getString(R.string.preOnboardingDaxDialog2Title) binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) @@ -300,8 +302,10 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p binding.daxDialogCta.comparisonChart.root.gone() TransitionManager.beginDelayedTransition(binding.daxDialogCta.cardView, AutoTransition()) binding.daxDialogCta.progressBarText.show() - binding.daxDialogCta.progressBarText.text = "2 / 3" + val maxPages = viewModel.getMaxPageCount() + binding.daxDialogCta.progressBarText.text = "2 / $maxPages" binding.daxDialogCta.progressBar.show() + binding.daxDialogCta.progressBar.max = maxPages binding.daxDialogCta.progressBar.progress = 2 val ctaText = it.getString(R.string.preOnboardingAddressBarTitle).run { if (onboardingDesignExperimentManager.isModifiedControlEnrolledAndEnabled()) { @@ -341,10 +345,12 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p binding.daxDialogCta.addressBarPosition.root.gone() TransitionManager.beginDelayedTransition(binding.daxDialogCta.cardView, AutoTransition()) binding.daxDialogCta.progressBarText.show() - binding.daxDialogCta.progressBarText.text = "3 / 3" + val maxPages = viewModel.getMaxPageCount() + binding.daxDialogCta.progressBarText.text = "3 / $maxPages" binding.daxDialogCta.progressBar.show() + binding.daxDialogCta.progressBar.max = maxPages binding.daxDialogCta.progressBar.progress = 3 - val ctaText = it.getString(R.string.preOnboardingAiChatAccessTitle) + val ctaText = it.getString(R.string.preOnboardingInputScreenTitle) binding.daxDialogCta.hiddenTextCta.text = ctaText.html(it) binding.daxDialogCta.primaryCta.alpha = MIN_ALPHA binding.daxDialogCta.duckAiInputScreenToggleContainer.show() @@ -363,14 +369,14 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p viewModel.onInputScreenOptionSelected(withAi = true) } - val descriptionText = it.getString(R.string.preOnboardingAiChatAccessDescription) + val descriptionText = it.getString(R.string.preOnboardingInputScreenDescription) binding.daxDialogCta.duckAiInputScreenToggleDescription.text = descriptionText.html(it) binding.daxDialogCta.duckAiInputScreenToggleDescription.show() binding.daxDialogCta.duckAiInputScreenToggleDescription.alpha = MIN_ALPHA afterAnimation = { binding.daxDialogCta.dialogTextCta.finishAnimation() - binding.daxDialogCta.primaryCta.text = it.getString(R.string.preOnboardingAiChatAccessButton) + binding.daxDialogCta.primaryCta.text = it.getString(R.string.preOnboardingInputScreenButton) binding.daxDialogCta.primaryCta.setOnClickListener { viewModel.onPrimaryCtaClicked(INPUT_SCREEN) } binding.daxDialogCta.primaryCta.animate().alpha(MAX_ALPHA).duration = ANIMATION_DURATION binding.daxDialogCta.duckAiInputScreenToggleContainer.animate().alpha(MAX_ALPHA).duration = ANIMATION_DURATION diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModel.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModel.kt index 89ba9c975a97..dcd62bfa436a 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModel.kt @@ -54,6 +54,7 @@ import com.duckduckgo.app.pixels.AppPixelName.PREONBOARDING_INTRO_SHOWN_UNIQUE import com.duckduckgo.app.pixels.AppPixelName.PREONBOARDING_RESUME_ONBOARDING_PRESSED import com.duckduckgo.app.pixels.AppPixelName.PREONBOARDING_SKIP_ONBOARDING_PRESSED import com.duckduckgo.app.pixels.AppPixelName.PREONBOARDING_SKIP_ONBOARDING_SHOWN_UNIQUE +import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter @@ -81,12 +82,24 @@ class WelcomePageViewModel @Inject constructor( private val appBuildConfig: AppBuildConfig, private val onboardingDesignExperimentManager: OnboardingDesignExperimentManager, private val onboardingStore: OnboardingStore, + private val androidBrowserConfigFeature: AndroidBrowserConfigFeature, ) : ViewModel() { private val _commands = Channel(1, DROP_OLDEST) val commands: Flow = _commands.receiveAsFlow() private var defaultAddressBarPosition: Boolean = true private var inputScreenSelected: Boolean = true + private var maxPageCount: Int = 2 + + init { + viewModelScope.launch(dispatchers.io()) { + maxPageCount = if (androidBrowserConfigFeature.showInputScreenOnboarding().isEnabled()) { + 3 + } else { + 2 + } + } + } sealed interface Command { data object ShowInitialReinstallUserDialog : Command @@ -169,7 +182,11 @@ class WelcomePageViewModel @Inject constructor( } else { onboardingDesignExperimentManager.fireAddressBarSetTopPixel() } - _commands.send(Command.ShowInputScreenDialog) + if (androidBrowserConfigFeature.showInputScreenOnboarding().isEnabled()) { + _commands.send(Command.ShowInputScreenDialog) + } else { + _commands.send(Finish) + } } } @@ -273,7 +290,7 @@ class WelcomePageViewModel @Inject constructor( } } INPUT_SCREEN -> { - // Pixel tracking can be added here if needed + // no-op } } } @@ -289,6 +306,10 @@ class WelcomePageViewModel @Inject constructor( inputScreenSelected = withAi } + fun getMaxPageCount(): Int { + return maxPageCount + } + fun loadDaxDialog() { viewModelScope.launch { if (isAppReinstall()) { diff --git a/app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt b/app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt index 90f8d8408f24..9e8d0ea7aaa8 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt @@ -212,4 +212,12 @@ interface AndroidBrowserConfigFeature { */ @Toggle.DefaultValue(DefaultFeatureValue.TRUE) fun useUrlPredictor(): Toggle + + /** + * @return `true` when the remote config has the global "showInputScreenOnboarding" androidBrowserConfig + * sub-feature flag enabled + * If the remote feature is not present defaults to `internal` + */ + @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL) + fun showInputScreenOnboarding(): Toggle } diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index eb962fb26802..bf1f7c270c27 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -65,4 +65,9 @@ Android Design System Demo + + + Want easy access to private AI chat? + Next + Settings > AI Features.]]> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8c539f3ab0d4..8ba5081165b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -782,9 +782,6 @@ Easy to see Bottom Easy to reach - Want easy access to private AI chat? - Next - Settings > AI Features.]]> Try a search! Your DuckDuckGo searches are always private. how to say "duck" in english diff --git a/app/src/test/java/com/duckduckgo/app/browser/newaddressbaroption/NewAddressBarOptionManagerTest.kt b/app/src/test/java/com/duckduckgo/app/browser/newaddressbaroption/NewAddressBarOptionManagerTest.kt index c57863360ccb..5325a3ea1cad 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/newaddressbaroption/NewAddressBarOptionManagerTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/newaddressbaroption/NewAddressBarOptionManagerTest.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.browser.newaddressbaroption import com.duckduckgo.app.browser.omnibar.OmnibarType 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.settings.db.SettingsDataStore import com.duckduckgo.common.test.CoroutineTestRule @@ -62,6 +63,9 @@ class NewAddressBarOptionManagerTest { @Mock private var settingsDataStoreMock: SettingsDataStore = mock() + @Mock + private var onboardingStoreMock: OnboardingStore = mock() + private val showNewAddressBarOptionAnnouncementFlow = MutableStateFlow(false) private val showOmnibarShortcutInAllStatesFlow = MutableStateFlow(false) private val showInputScreenFlow = MutableStateFlow(false) @@ -85,6 +89,7 @@ class NewAddressBarOptionManagerTest { remoteMessagingRepositoryMock, newAddressBarOptionDataStoreMock, settingsDataStoreMock, + onboardingStoreMock, coroutineTestRule.testDispatcherProvider, ) } @@ -294,6 +299,28 @@ class NewAddressBarOptionManagerTest { verify(newAddressBarOptionDataStoreMock).setAsShown() } + @Test + fun `when input screen selection exists then showChoiceScreen does not show dialog`() = + runTest { + setupAllConditionsMet() + whenever(onboardingStoreMock.getInputScreenSelection()).thenReturn(true) + + testee.showChoiceScreen(mock(), isLaunchedFromExternal = false) + + verify(duckChatMock, never()).showNewAddressBarOptionChoiceScreen(any(), any()) + } + + @Test + fun `when input screen selection is false then showChoiceScreen does not show dialog`() = + runTest { + setupAllConditionsMet() + whenever(onboardingStoreMock.getInputScreenSelection()).thenReturn(false) + + testee.showChoiceScreen(mock(), isLaunchedFromExternal = false) + + verify(duckChatMock, never()).showNewAddressBarOptionChoiceScreen(any(), any()) + } + private suspend fun setupAllConditionsMet() { whenever(duckChatMock.isEnabled()).thenReturn(true) whenever(userStageStoreMock.getUserAppStage()).thenReturn(AppStage.ESTABLISHED) @@ -304,5 +331,6 @@ class NewAddressBarOptionManagerTest { whenever(remoteMessagingRepositoryMock.dismissedMessages()).thenReturn(emptyList()) whenever(settingsDataStoreMock.omnibarType).thenReturn(OmnibarType.SINGLE_TOP) whenever(newAddressBarOptionDataStoreMock.wasValidated()).thenReturn(true) + whenever(onboardingStoreMock.getInputScreenSelection()).thenReturn(null) } } diff --git a/app/src/test/java/com/duckduckgo/app/onboarding/OnboardingInputScreenSelectionObserverTest.kt b/app/src/test/java/com/duckduckgo/app/onboarding/OnboardingInputScreenSelectionObserverTest.kt new file mode 100644 index 000000000000..21e7b21f4636 --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/onboarding/OnboardingInputScreenSelectionObserverTest.kt @@ -0,0 +1,123 @@ +/* + * 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.onboarding + +import com.duckduckgo.app.onboarding.store.AppStage +import com.duckduckgo.app.onboarding.store.OnboardingStore +import com.duckduckgo.app.onboarding.store.UserStageStore +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.duckchat.api.DuckChat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class OnboardingInputScreenSelectionObserverTest { + @get:Rule + val coroutineRule = CoroutineTestRule() + + private val mockAppCoroutineScope: CoroutineScope = coroutineRule.testScope + private val mockUserStageStore: UserStageStore = mock() + private val mockOnboardingStore: OnboardingStore = mock() + private val mockDuckChat: DuckChat = mock() + private val dispatcherProvider: DispatcherProvider = coroutineRule.testDispatcherProvider + + private val userAppStageFlow = MutableStateFlow(AppStage.NEW) + + @Test + fun whenUserStageIsEstablishedAndInputScreenSelectionIsTrueThenSetInputScreenUserSettingToTrue() = + runTest { + whenever(mockUserStageStore.userAppStageFlow()).thenReturn(userAppStageFlow) + whenever(mockOnboardingStore.getInputScreenSelection()).thenReturn(true) + + OnboardingInputScreenSelectionObserver( + mockAppCoroutineScope, + dispatcherProvider, + mockUserStageStore, + mockOnboardingStore, + mockDuckChat, + ) + + userAppStageFlow.value = AppStage.ESTABLISHED + + verify(mockDuckChat).setInputScreenUserSetting(true) + } + + @Test + fun whenUserStageIsEstablishedAndInputScreenSelectionIsFalseThenSetInputScreenUserSettingToFalse() = + runTest { + whenever(mockUserStageStore.userAppStageFlow()).thenReturn(userAppStageFlow) + whenever(mockOnboardingStore.getInputScreenSelection()).thenReturn(false) + + OnboardingInputScreenSelectionObserver( + mockAppCoroutineScope, + dispatcherProvider, + mockUserStageStore, + mockOnboardingStore, + mockDuckChat, + ) + + userAppStageFlow.value = AppStage.ESTABLISHED + + verify(mockDuckChat).setInputScreenUserSetting(false) + } + + @Test + fun whenUserStageIsEstablishedAndInputScreenSelectionIsNullThenDoNotSetInputScreenUserSetting() = + runTest { + whenever(mockUserStageStore.userAppStageFlow()).thenReturn(userAppStageFlow) + whenever(mockOnboardingStore.getInputScreenSelection()).thenReturn(null) + + OnboardingInputScreenSelectionObserver( + mockAppCoroutineScope, + dispatcherProvider, + mockUserStageStore, + mockOnboardingStore, + mockDuckChat, + ) + + userAppStageFlow.value = AppStage.ESTABLISHED + + verify(mockDuckChat, never()).setInputScreenUserSetting(any()) + } + + @Test + fun whenUserStageIsNotEstablishedThenDoNotSetInputScreenUserSetting() = + runTest { + whenever(mockUserStageStore.userAppStageFlow()).thenReturn(userAppStageFlow) + whenever(mockOnboardingStore.getInputScreenSelection()).thenReturn(true) + + OnboardingInputScreenSelectionObserver( + mockAppCoroutineScope, + dispatcherProvider, + mockUserStageStore, + mockOnboardingStore, + mockDuckChat, + ) + + userAppStageFlow.value = AppStage.DAX_ONBOARDING + + verify(mockDuckChat, never()).setInputScreenUserSetting(any()) + } +} diff --git a/app/src/test/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModelTest.kt index a5c3b92b6f39..c6c45565136c 100644 --- a/app/src/test/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/onboarding/ui/page/WelcomePageViewModelTest.kt @@ -22,6 +22,7 @@ import app.cash.turbine.test import com.duckduckgo.app.browser.omnibar.OmnibarType import com.duckduckgo.app.global.DefaultRoleBrowserDialog import com.duckduckgo.app.global.install.AppInstallStore +import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.Finish import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.OnboardingSkipped import com.duckduckgo.app.onboarding.ui.page.WelcomePageViewModel.Command.ShowAddressBarPositionDialog @@ -42,12 +43,15 @@ import com.duckduckgo.app.pixels.AppPixelName.PREONBOARDING_INTRO_SHOWN_UNIQUE import com.duckduckgo.app.pixels.AppPixelName.PREONBOARDING_RESUME_ONBOARDING_PRESSED import com.duckduckgo.app.pixels.AppPixelName.PREONBOARDING_SKIP_ONBOARDING_PRESSED import com.duckduckgo.app.pixels.AppPixelName.PREONBOARDING_SKIP_ONBOARDING_SHOWN_UNIQUE +import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Unique import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory +import com.duckduckgo.feature.toggles.api.Toggle import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Rule @@ -68,6 +72,25 @@ class WelcomePageViewModelTest { private val mockSettingsDataStore: SettingsDataStore = mock() private val mockAppBuildConfig: AppBuildConfig = mock() private val mockOnboardingDesignExperimentManager: OnboardingDesignExperimentManager = mock() + private val mockOnboardingStore: OnboardingStore = mock() + private val mockAndroidBrowserConfigFeature: AndroidBrowserConfigFeature = FakeFeatureToggleFactory.create( + AndroidBrowserConfigFeature::class.java, + ) + + private fun createViewModel(): WelcomePageViewModel { + return WelcomePageViewModel( + mockDefaultRoleBrowserDialog, + mockContext, + mockPixel, + mockAppInstallStore, + mockSettingsDataStore, + coroutineRule.testDispatcherProvider, + mockAppBuildConfig, + mockOnboardingDesignExperimentManager, + mockOnboardingStore, + mockAndroidBrowserConfigFeature, + ) + } private val testee: WelcomePageViewModel by lazy { WelcomePageViewModel( @@ -79,6 +102,8 @@ class WelcomePageViewModelTest { coroutineRule.testDispatcherProvider, mockAppBuildConfig, mockOnboardingDesignExperimentManager, + mockOnboardingStore, + mockAndroidBrowserConfigFeature, ) } @@ -387,4 +412,50 @@ class WelcomePageViewModelTest { } verify(mockPixel).fire(PREONBOARDING_RESUME_ONBOARDING_PRESSED) } + + @Test + fun whenOnPrimaryCtaClickedWithInputScreenSelectedThenStoreSelectionAndFinish() = + runTest { + mockAndroidBrowserConfigFeature.showInputScreenOnboarding().setRawStoredState(Toggle.State(enable = true)) + testee.onInputScreenOptionSelected(true) + testee.onPrimaryCtaClicked(PreOnboardingDialogType.INPUT_SCREEN) + + testee.commands.test { + val command = awaitItem() + Assert.assertTrue(command is Finish) + } + verify(mockOnboardingStore).storeInputScreenSelection(true) + } + + @Test + fun whenOnPrimaryCtaClickedWithInputScreenNotSelectedThenStoreSelectionAndFinish() = + runTest { + mockAndroidBrowserConfigFeature.showInputScreenOnboarding().setRawStoredState(Toggle.State(enable = true)) + testee.onInputScreenOptionSelected(false) + testee.onPrimaryCtaClicked(PreOnboardingDialogType.INPUT_SCREEN) + + testee.commands.test { + val command = awaitItem() + Assert.assertTrue(command is Finish) + } + verify(mockOnboardingStore).storeInputScreenSelection(false) + } + + @Test + fun whenInputScreenOnboardingIsEnabledThenGetMaxPageCountReturns3() = + runTest { + mockAndroidBrowserConfigFeature.showInputScreenOnboarding().setRawStoredState(Toggle.State(enable = true)) + val viewModel = createViewModel() + + Assert.assertEquals(3, viewModel.getMaxPageCount()) + } + + @Test + fun whenInputScreenOnboardingIsDisabledThenGetMaxPageCountReturns2() = + runTest { + mockAndroidBrowserConfigFeature.showInputScreenOnboarding().setRawStoredState(Toggle.State(enable = false)) + val viewModel = createViewModel() + + Assert.assertEquals(2, viewModel.getMaxPageCount()) + } } diff --git a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/messaging/fakes/FakeDuckChat.kt b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/messaging/fakes/FakeDuckChat.kt index ce7a189c204d..cdaa59b129d4 100644 --- a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/messaging/fakes/FakeDuckChat.kt +++ b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/messaging/fakes/FakeDuckChat.kt @@ -65,6 +65,10 @@ class FakeDuckChat( // No-op for testing } + override suspend fun setInputScreenUserSetting(enabled: Boolean) { + // No-op for testing + } + fun setEnabled(enabled: Boolean) { this.enabled = enabled }