Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5edc751
feat(onboarding): add brand-design contextual dialog scaffolding
mikescamell Apr 27, 2026
e9aa988
feat(onboarding): migrate DaxSerpCta to brand-design contextual dialog
mikescamell Apr 27, 2026
d4bb60d
i18n(onboarding): translate brand-design SERP title and body across 2…
mikescamell Apr 27, 2026
9a95d78
test(onboarding): add unit and telemetry-flow assertions for SERP bra…
mikescamell Apr 27, 2026
ffcae4e
fix(onboarding): clear all in-context CTA flags from dev-settings onb…
mikescamell Apr 28, 2026
35e50a0
feat(onboarding): add SERP CTA background banner
mikescamell Apr 30, 2026
2edf703
feat(onboarding): wire DaxSerpCta to its background banner
mikescamell Apr 30, 2026
33dcedd
feat(onboarding): add background banner slide animations to contextua…
mikescamell May 1, 2026
0a7e9c2
feat(onboarding): handle device rotation for brand-design contextual …
mikescamell May 6, 2026
eeeb174
feat(onboarding): add bottom-edge shadow to brand-design contextual d…
mikescamell May 6, 2026
5a53cd4
fix: replace SingleLiveEvent.postValue with setValue on main (#8439)
mikescamell May 7, 2026
ba7c924
refactor(onboarding): name brand-design contextual dialog fade-in sta…
mikescamell May 7, 2026
0471df4
fix(onboarding): cancel brand-design dialog animators on dismiss
mikescamell May 7, 2026
b82e2a1
refactor(onboarding): drop redundant self() check in isBrandDesignUpd…
mikescamell May 11, 2026
4c1b48a
refactor(onboarding): move contextual options height into the CTA
mikescamell May 12, 2026
db38d6e
style(onboarding): name params on brand-design showOnboardingCta call
mikescamell May 12, 2026
5dd1afe
refactor(onboarding): split showOnboardingDialogCta by CTA type
mikescamell May 12, 2026
99c7d6a
refactor(onboarding): centralize brand-design dialog animation cancel
mikescamell May 12, 2026
fc1da61
refactor(onboarding): adopt isAnimating setter pattern in contextual CTA
mikescamell May 12, 2026
153e08e
refactor(onboarding): gate dismiss-button fade-in on its actual alpha
mikescamell May 12, 2026
f89b429
docs(onboarding): fix misplaced title-slot kdoc on contextual CTA
mikescamell May 12, 2026
7f69bdd
refactor(onboarding): extract BackgroundBanner from contextual CTA
mikescamell May 12, 2026
e0a52a3
test(onboarding): drop BackgroundBanner slideOut-happy-path JVM test
mikescamell May 12, 2026
2fa2a3c
fix(onboarding): harden brand-design dialog cancel + tap-to-skip paths
mikescamell May 12, 2026
fe8aaa8
test(onboarding): pass DuckAiOnboardingExperimentMetrics to CtaViewModel
mikescamell May 12, 2026
98177d6
fix(onboarding): cancel banner slide-in animator on dismiss
mikescamell May 12, 2026
ad9f776
refactor(onboarding): use keyline_0 for contextual options row spacing
mikescamell May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
<attr name="onboardingIconsSecondary" format="color" />
<attr name="onboardingIconsTertiary" format="color" />
<attr name="onboardingControlFillTertiary" format="color" />
<attr name="onboardingShadowPurple" format="color" />
<attr name="onboardingShadowBlue" format="color" />
<color name="onboardingBubbleShadowColor">#08000000</color>

<attr name="selectionButtonStyle" format="reference"/>
Expand All @@ -58,6 +60,8 @@
<item name="onboardingAccentGlowPrimary">#332F95EE</item> <!--20% Alpha-->
<item name="onboardingAccentTextPrimary">@color/pondwater60</item>
<item name="onboardingControlFillTertiary">@color/black12</item>
<item name="onboardingShadowPurple">#0F3E228C</item> <!--6% Alpha-->
<item name="onboardingShadowBlue">#171E42A4</item> <!--9% Alpha-->

<!--Icons-->
<item name="onboardingIconsPrimary">#D6242323</item> <!--84% Alpha-->
Expand Down Expand Up @@ -121,6 +125,8 @@
<item name="onboardingAccentGlowPrimary">#2975B6EB</item> <!--16% Alpha-->
<item name="onboardingAccentTextPrimary">@color/pondwater40</item>
<item name="onboardingControlFillTertiary">@color/white24</item>
<item name="onboardingShadowPurple">#0F070019</item> <!--6% Alpha-->
<item name="onboardingShadowBlue">#17051133</item> <!--9% Alpha-->

<!--Icons-->
<item name="onboardingIconsPrimary">#D6FBFAF9</item> <!--84% Alpha-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ class OnboardingDevSettingsViewModel @Inject constructor(

private suspend fun visibleCtaIds(): List<CtaId> {
val requiredDialogs = ctaViewModel.requiredDaxOnboardingCtas()
val extraDialogs = listOf(CtaId.DAX_INTRO_VISIT_SITE, CtaId.ADD_WIDGET).filterNot { it in requiredDialogs }
val extraDialogs = listOf(
CtaId.DAX_INTRO_VISIT_SITE,
CtaId.DAX_DIALOG_NETWORK,
CtaId.DAX_DIALOG_OTHER,
CtaId.ADD_WIDGET,
).filterNot { it in requiredDialogs }
return requiredDialogs + extraDialogs
}

Expand Down
98 changes: 91 additions & 7 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ import android.provider.MediaStore
import android.text.Spanned
import android.view.ContextMenu
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.webkit.PermissionRequest
import android.webkit.SslErrorHandler
Expand Down Expand Up @@ -758,6 +760,11 @@ class BrowserTabFragment :
private val daxDialogInContext
get() = binding.includeOnboardingInContextDaxDialog

private val daxDialogInContextBrandDesign
get() = binding.includeOnboardingInContextDaxDialogBrandDesign

private var lastOrientation: Int = Configuration.ORIENTATION_UNDEFINED

private val newTabReturnHatchView
get() = binding.includeNewBrowserTab.newTabReturnHatchView

Expand Down Expand Up @@ -1030,6 +1037,7 @@ class BrowserTabFragment :
super.onCreate(savedInstanceState)
logcat { "onCreate called for tabId=$tabId" }

lastOrientation = resources.configuration.orientation
removeDaxDialogFromActivity()
renderer = BrowserTabFragmentRenderer()
voiceSearchLauncher.registerResultsCallback(this, requireActivity(), BROWSER) {
Expand Down Expand Up @@ -2813,7 +2821,7 @@ class BrowserTabFragment :
is Command.LaunchScreen -> launchScreen(it.screen, it.payload)
is Command.HideOnboardingDaxDialog -> hideOnboardingDaxDialog(it.onboardingCta)
is Command.HideBrokenSitePromptCta -> hideBrokenSitePromptCta(it.brokenSitePromptDialogCta)
is Command.HideOnboardingDaxBubbleCta -> hideOnboardingDaxBubbleCta()
is Command.HideOnboardingDaxBubbleCta -> hideOnboardingDaxBubbleCta(it.daxBubbleCta)
is Command.ShowRemoveSearchSuggestionDialog -> showRemoveSearchSuggestionDialog(it.suggestion)
is Command.AutocompleteItemRemoved -> autocompleteItemRemoved()
is Command.OpenDuckPlayerSettings -> globalActivityStarter.start(binding.root.context, DuckPlayerSettingsNoParams)
Expand Down Expand Up @@ -2842,6 +2850,7 @@ class BrowserTabFragment :
}
is Command.SetOnboardingDialogBackground -> setOnboardingDialogBackgroundRes(it.backgroundRes)
is Command.SetOnboardingDialogBackgroundColor -> setOnboardingDialogBackgroundColor(it.colorRes)
is Command.ReinflateBrandDesignContextualDialog -> renderer.reinflateContextualBrandDesignDialog()
is Command.LaunchFireDialogFromOnboardingDialog -> {
hideOnboardingDaxDialog(it.onboardingCta)
browserActivity?.launchFire()
Expand Down Expand Up @@ -4085,8 +4094,8 @@ class BrowserTabFragment :
brokenSitePromptDialogCta.hideOnboardingCta(binding)
}

private fun hideOnboardingDaxBubbleCta() {
hideDaxBubbleCta()
private fun hideOnboardingDaxBubbleCta(cta: DaxBubbleCta?) {
hideDaxBubbleCta(cta)
renderer.showNewTab()
// When the input-screen feature is on, the omnibar's click catcher disables the text
// input, so requestFocus() can't succeed. Showing the IME there leaves the keyboard up
Expand All @@ -4097,7 +4106,8 @@ class BrowserTabFragment :
}
}

private fun hideDaxBubbleCta() {
private fun hideDaxBubbleCta(cta: DaxBubbleCta?) {
(cta as? DaxBubbleCta.BrandDesignUpdateBubbleCta)?.cancelRunningAnimations()
newBrowserTab.browserBackground.setImageResource(0)
val wasBrandDesign = newBrowserTab.rebrandBrowserBackground.isVisible
newBrowserTab.rebrandBrowserBackground.apply {
Expand Down Expand Up @@ -4799,9 +4809,12 @@ class BrowserTabFragment :
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)

val orientationChanged = lastOrientation != newConfig.orientation
lastOrientation = newConfig.orientation

renderer.renderHomeCta()
recreateBrowserMenu()
viewModel.onConfigurationChanged()
viewModel.onConfigurationChanged(orientationChanged)
}

fun onBackPressed(isCustomTab: Boolean = false): Boolean {
Expand Down Expand Up @@ -5825,6 +5838,7 @@ class BrowserTabFragment :
}

renderIfChanged(viewState, lastSeenCtaViewState) {
val previousCta = lastSeenCtaViewState?.cta
lastSeenCtaViewState = viewState
when {
viewState.cta != null -> {
Expand All @@ -5837,18 +5851,44 @@ class BrowserTabFragment :
}

viewState.isOnboardingCompleteInNewTabPage && !viewState.isErrorShowing -> {
hideDaxBubbleCta()
hideDaxBubbleCta(previousCta as? DaxBubbleCta)
showNewTab()
}
}
}
}

private fun showCta(configuration: Cta) {
fun reinflateContextualBrandDesignDialog() {
if (!isAdded) return
val cta = lastSeenCtaViewState?.cta as? OnboardingDaxDialogCta.BrandDesignContextualDaxDialogCta ?: return

val existingRoot = daxDialogInContextBrandDesign.root
if (!existingRoot.isVisible) return
val parent = existingRoot.parent as? ViewGroup ?: return
val fresh = LayoutInflater.from(requireContext())
.inflate(R.layout.include_onboarding_in_context_dax_dialog_brand_design_update, parent, false)
as? ViewGroup ?: return

existingRoot.setPadding(fresh.paddingLeft, fresh.paddingTop, fresh.paddingRight, fresh.paddingBottom)

existingRoot.setOnClickListener(null)
existingRoot.removeAllViews()
while (fresh.childCount > 0) {
val child = fresh.getChildAt(0)
fresh.removeViewAt(0)
existingRoot.addView(child)
}

showCta(cta, instantShow = true)
}

private fun showCta(configuration: Cta, instantShow: Boolean = false) {
when (configuration) {
is HomePanelCta -> showBottomSheetCta(configuration)
is SubscriptionPromoModalCta -> showPrivacyProSkippedOnboardingBottomSheet(configuration)
is DaxBubbleCta -> showDaxOnboardingBubbleCta(configuration)
is OnboardingDaxDialogCta.BrandDesignContextualDaxDialogCta ->
showOnboardingDialogCta(configuration, instantShow = instantShow)
is OnboardingDaxDialogCta -> showOnboardingDialogCta(configuration)
is BrokenSitePromptDialogCta -> showBrokenSitePromptCta(configuration)
}
Expand Down Expand Up @@ -5936,6 +5976,14 @@ class BrowserTabFragment :
@SuppressLint("ClickableViewAccessibility")
private fun showOnboardingDialogCta(configuration: OnboardingDaxDialogCta) {
hideNewTab()
// Brand-design path disables APPEARING/DISAPPEARING on this container; the legacy
// path expects them enabled, so restore here in case the previous CTA was brand-design.
binding.daxDialogOnboardingCtaContent.layoutTransition.apply {
enableTransitionType(LayoutTransition.APPEARING)
enableTransitionType(LayoutTransition.DISAPPEARING)
}
// Both layouts live in the tree; only one should be visible at a time.
daxDialogInContextBrandDesign.root.gone()
val onTypingAnimationFinished =
if (configuration is OnboardingDaxDialogCta.DaxTrackersBlockedCta) {
{ viewModel.onOnboardingDaxTypingAnimationFinished() }
Expand All @@ -5962,6 +6010,36 @@ class BrowserTabFragment :
viewModel.onCtaShown()
}

@SuppressLint("ClickableViewAccessibility")
private fun showOnboardingDialogCta(
configuration: OnboardingDaxDialogCta.BrandDesignContextualDaxDialogCta,
instantShow: Boolean = false,
) {
hideNewTab()
binding.daxDialogOnboardingCtaContent.layoutTransition.apply {
disableTransitionType(LayoutTransition.APPEARING)
disableTransitionType(LayoutTransition.DISAPPEARING)
}
// Both layouts live in the tree; only one should be visible at a time.
daxDialogInContext.root.gone()
configuration.showOnboardingCta(
binding = binding,
onPrimaryCtaClicked = { viewModel.onUserClickCtaOkButton(configuration) },
onSecondaryCtaClicked = { viewModel.onUserClickCtaSecondaryButton(configuration) },
// The base class invokes onTypingAnimationSettled exactly once.
// CTA-type-specific notifications (e.g. DaxTrackersBlocked) live on the
// subclass override, not on the fragment.
onTypingAnimationFinished = { viewModel.onOnboardingDaxTypingAnimationFinished() },
onSuggestedOptionClicked = { option: DaxDialogIntroOption -> userEnteredQuery(option.link) },
onDismissCtaClicked = { viewModel.onUserClickCtaDismissButton(configuration) },
instantShow = instantShow,
)
viewModel.setOnboardingDialogBackground(appTheme.isLightModeEnabled())
if (!instantShow) {
viewModel.onCtaShown()
}
}

@SuppressLint("ClickableViewAccessibility")
private fun showBrokenSitePromptCta(configuration: BrokenSitePromptDialogCta) {
hideNewTab()
Expand Down Expand Up @@ -6100,6 +6178,12 @@ class BrowserTabFragment :
private fun hideDaxCta() {
daxDialogInContext.dialogTextCta.cancelAnimation()
daxDialogInContext.daxCtaContainer.gone()
val cta = lastSeenCtaViewState?.cta as? OnboardingDaxDialogCta.BrandDesignContextualDaxDialogCta
if (cta != null) {
cta.hideOnboardingCta(binding)
} else {
OnboardingDaxDialogCta.BrandDesignContextualDaxDialogCta.hideContainer(binding)
}
}

fun renderHomeCta() {
Expand Down
Loading
Loading