Skip to content

Onboarding Design Update: Input choice screen#8223

Merged
mikescamell merged 31 commits intodevelopfrom
feature/mike/onboading-brand-design-updates/ai-chat-toggle
Apr 13, 2026
Merged

Onboarding Design Update: Input choice screen#8223
mikescamell merged 31 commits intodevelopfrom
feature/mike/onboading-brand-design-updates/ai-chat-toggle

Conversation

@mikescamell
Copy link
Copy Markdown
Contributor

@mikescamell mikescamell commented Apr 8, 2026

Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1212699261164265?focus=true

Description

Implements the AI chat toggle (input screen) for the onboarding brand design update and adds left wing animation support. This is the third dialog step in the redesigned onboarding flow, allowing users to choose between AI-enhanced search or search-only mode.

Key changes:

  • AI chat toggle screen with crossfade image transitions between selected/unselected states
  • Left wing Lottie animation with play, dismiss, and restore logic for the input screen transition
  • Tap-to-skip dialog animations across all onboarding steps
  • Bobbing Dax exit animation when transitioning to the input screen
  • Crossfade animation fix to prevent stacking ViewPropertyAnimators under rapid toggling
  • Pixel parity audit confirming all 19 onboarding pixels match between old and new implementations
  • 3 missing pixel verification tests added to BrandDesignUpdatePageViewModelTest

Steps to test this PR

Designs

Apply patch

Subject: [PATCH] onboarding
---
Index: app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingViewModel.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingViewModel.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingViewModel.kt
--- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingViewModel.kt	(revision 06d747bcc29beada0a444df876b05f55f0dbb4d5)
+++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingViewModel.kt	(date 1775628931668)
@@ -45,7 +45,7 @@
     val viewState = _viewState.asStateFlow()
 
     fun initializePages() {
-        pageLayoutManager.buildPageBlueprints()
+        pageLayoutManager.buildBrandDesignUpdatePageBlueprints()
     }
 
     fun pageCount(): Int {
@@ -70,7 +70,7 @@
     }
 
     fun initializeOnboardingSkipper() {
-        if (!appBuildConfig.canSkipOnboarding) return
+        return
 
         // delay showing skip button until privacy config downloaded
         viewModelScope.launch {

AI chat toggle screen

  • Go through onboarding to the AI chat toggle screen (after address bar position)
  • Verify title typing animation plays on entry
  • Verify AI chat option is selected by default
  • Toggle between AI chat and search-only options
  • Verify crossfade image transition animates smoothly between states
  • Rapidly toggle options and verify no visual glitches from stacked animations
  • Rotate the device and verify the dialog restores correctly with the selected option preserved
  • Verify step indicator shows correct step count (3/3)
  • Tap primary CTA with AI chat selected → verify flow completes

Left wing animation

  • Verify left wing Lottie animation plays during the input screen transition
  • Verify left wing dismisses correctly when leaving the input screen
  • Rotate the device on the input screen and verify left wing state restores

Tap-to-skip dialog animations

  • On each dialog step, tap the dialog area to skip the typing/fade-in animation
  • Verify the dialog snaps to its final state cleanly without visual artifacts
  • Verify tapping does nothing if animation has already completed

Bobbing Dax exit

  • On the address bar position screen, verify bobbing Dax slides out when transitioning to the input screen

Pixel testing

Setup

  • Install a debug build (./gradlew installInternalDebug)
  • Clear app data to trigger the onboarding flow
  • Open logcat and filter for Pixel to observe pixel fire events

New user flow - dialog shown pixels

  • Launch app fresh → verify m_preonboarding_intro_shown_unique fires in logcat
  • Tap primary CTA → verify m_preonboarding_comparison_chart_shown_unique fires

New user flow - default browser pixels

  • From comparison chart, tap primary CTA → verify m_preonboarding_choose_browser_pressed fires with defaultBrowser parameter
  • If default browser dialog appears and you set DDG as default → verify m_db_s fires with fo=true
  • If default browser dialog appears and you dismiss it → verify m_db_ns fires with fo=true

New user flow - address bar position pixels

  • Verify m_preonboarding_address_bar_position_dialog_shown_unique fires when address bar screen appears
  • Select bottom address bar and tap primary CTA → verify m_preonboarding_bottom_address_bar_selected_unique fires
  • (If split omnibar enabled) Select split and tap primary CTA → verify m_preonboarding_split_address_bar_selected_unique fires

New user flow - input screen pixels (if feature enabled)

  • Verify m_preonboarding_choose_search_experience_impressions_unique fires when input screen appears
  • Select AI chat option and tap primary CTA → verify m_preonboarding_aichat_selected fires
  • OR select search-only and tap primary CTA → verify m_preonboarding_search_only_selected fires

Reinstall user flow

  • Clear app data, simulate reinstall (or use a device where DDG was previously installed)
  • Launch app → verify m_preonboarding_intro_reinstall_user_shown_unique fires
  • Tap "Skip onboarding" secondary CTA → verify m_preonboarding_skip-onboarding-pressed fires
  • Verify m_preonboarding_skip_onboarding_shown_unique fires when skip confirmation appears
  • Tap "Skip" to confirm → verify m_preonboarding_confirm-skip-onboarding-pressed fires
  • OR tap "Resume" → verify m_preonboarding_resume-onboarding-pressed fires

Notification permission pixels

  • Verify m_notification_runtime_permission_shown fires when notification permission dialog is shown
  • Grant notification permission → verify mnot_e fires with fromOnboarding=true

UI changes

See here for demoes


Note

Medium Risk
Touches core onboarding flow and adds new animation/interaction paths (including touch interception), which could affect navigation, state restore, and visual correctness across devices/orientations.

Overview
Adds a new Input Screen step to the brand design update onboarding flow, letting users choose between search-only and AI-enhanced input with animated crossfade toggles and persisted selection.

Updates onboarding visuals and motion: introduces a new OnboardingBackgroundStep.InputType, adds a left-wing Lottie decoration, animates bobbing Dax out when transitioning to the new step, and adds tap-to-skip support that snaps typing/fade/checkmark animations to their end state.

Extends onboarding theming/resources to support the new UI (new alpha colors and onboarding attrs), refactors omnibar option images to crossfade via front/back ImageViews, and adds tests for missing default-browser pixel cases and the new skip-animation command.

Reviewed by Cursor Bugbot for commit 58bfeb9. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown
Contributor Author

mikescamell commented Apr 8, 2026

@mikescamell mikescamell changed the title Onboarding Design Update: Input choice dialog Onboarding Design Update: Input choice screen Apr 8, 2026
@mikescamell mikescamell force-pushed the feature/mike/onboading-brand-design-updates/ai-chat-toggle branch from 2ff2148 to bd70043 Compare April 8, 2026 21:55
@LukasPaczos LukasPaczos self-assigned this Apr 9, 2026
@mikescamell mikescamell changed the base branch from feature/mike/onboading-brand-design-updates/address-bar-position to graphite-base/8223 April 9, 2026 10:43
mikescamell and others added 17 commits April 9, 2026 17:43
…date

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… screen

The dax was animated in during the address bar step but had no exit
animation. Now it slides left and fades out in sync with the background,
using EXIT_DURATION and exitAlpha() exposed from OnboardingBackgroundAnimator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ctions

Replaces inline animation blocks with animateBobbingDaxIn() and
animateBobbingDaxOut() to reduce duplication and make call sites readable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tapping the dialog card during animations completes them instantly.
The card tap flows through the ViewModel (onDialogTapped →
SkipDialogAnimation command) to skipCurrentDialogAnimation() which
finishes any active typing, ends running fade-in AnimatorSets, and
snaps comparison chart check icons to their final state.

Invisible-but-clickable child views (option containers, CTA button)
are disabled during animation to prevent them from consuming touch
events meant for the skip handler, and re-enabled when the fade-in
completes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…delTest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ewModelTest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…atePageViewModelTest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… to prevent stacking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d remove unused dismissLeftWingAnimation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mikescamell and others added 5 commits April 9, 2026 17:43
…ner to match other screens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…troyView cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…add onDialogTapped test

Align two remaining tablet checks in INPUT_SCREEN with the rest of the file,
and add unit test for the SkipDialogAnimation command.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mikescamell mikescamell force-pushed the feature/mike/onboading-brand-design-updates/ai-chat-toggle branch from 8b2079b to edbd008 Compare April 9, 2026 16:43
@mikescamell mikescamell changed the base branch from graphite-base/8223 to develop April 9, 2026 16:44
…rotation

The showDialogWithoutAnimation INPUT_SCREEN path was missing
setArrowAnimationTarget/Fraction and translationZ that COMPARISON_CHART
and ADDRESS_BAR_POSITION both set on rotation restore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…_SCREEN

AutoTransition runs Fade OUT + ChangeBounds + Fade IN sequentially,
tripling the 400ms duration to 1200ms. Use ChangeBounds to match the
other dialog steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 95e17a2. Configure here.

Copy link
Copy Markdown
Contributor

@LukasPaczos LukasPaczos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works as expected! Two things I wanted to discuss before we merge:

Click management

Generally (beside the comment below), skipping works but it's ~30 scattered isClickable = false/true calls across 5 dialog types, with unlock logic buried in nested animation callbacks. It seems to be correct now but very fragile and hard to extend/update if we ever need to touch the layouts.

The root cause of the complexity seems to be that each child view individually opts out of touches so that the card can handle them. What if we flip that - have the top level card (or screen) container that blocks touches from reaching children during animation?

For example, replace cardContainer's LinearLayout with a small custom subclass:

class TouchInterceptingLinearLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null,
) : LinearLayout(context, attrs) {

    var interceptChildTouches = false

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        return interceptChildTouches || super.onInterceptTouchEvent(ev)
    }
}

When interceptChildTouches = true, children never receive touches. Touches go to the LinearLayout's own onTouchEvent, which fires the existing skip click listener
(viewModel.onDialogTapped()).

I generated a POC in #8241 - feel free to check it out and merge back here if you also think it'd be an improvement.

Dax wing during rotation

Minor issue - when screen is rotated mid-transition, the wing is gone on the resulting input selection screen. Rotating back again make it appear. Not a big deal but let's double-check there aren't any other issue this could cause.

Screen_recording_20260410_091534.mp4

@mikescamell
Copy link
Copy Markdown
Contributor Author

Works as expected! Two things I wanted to discuss before we merge:

Click management

Generally (beside the comment below), skipping works but it's ~30 scattered isClickable = false/true calls across 5 dialog types, with unlock logic buried in nested animation callbacks. It seems to be correct now but very fragile and hard to extend/update if we ever need to touch the layouts.

The root cause of the complexity seems to be that each child view individually opts out of touches so that the card can handle them. What if we flip that - have the top level card (or screen) container that blocks touches from reaching children during animation?

For example, replace cardContainer's LinearLayout with a small custom subclass:

class TouchInterceptingLinearLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null,
) : LinearLayout(context, attrs) {

    var interceptChildTouches = false

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        return interceptChildTouches || super.onInterceptTouchEvent(ev)
    }
}

When interceptChildTouches = true, children never receive touches. Touches go to the LinearLayout's own onTouchEvent, which fires the existing skip click listener (viewModel.onDialogTapped()).

I generated a POC in #8241 - feel free to check it out and merge back here if you also think it'd be an improvement.

Dax wing during rotation

Minor issue - when screen is rotated mid-transition, the wing is gone on the resulting input selection screen. Rotating back again make it appear. Not a big deal but let's double-check there aren't any other issue this could cause.

Screen_recording_20260410_091534.mp4

I actually attempted something like you mentioned in regards to blocking touches but it broke some animations and in the interest of time i abandoned it. Let me get a look at your POC 👍

mikescamell and others added 2 commits April 10, 2026 17:51
…ation

Aligns with production onboarding behavior where tapping anywhere on
screen skips the animation, not just tapping on the dialog card.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nterceptingLinearLayout

Replace ~28 individual isClickable = false/true calls across 5 dialog
types with a single TouchInterceptingLinearLayout that intercepts child
touches when isAnimating is true. Based on POC from #8241.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mikescamell
Copy link
Copy Markdown
Contributor Author

mikescamell commented Apr 10, 2026

Click management

Generally (beside the comment below), skipping works but it's ~30 scattered isClickable = false/true calls across 5 dialog types...

Thanks Lukasz, great suggestion! I've adopted your TouchInterceptingLinearLayout approach from #8241 and replaced all the scattered isClickable calls with the single interceptChildTouches flag driven by the isAnimating setter. Much cleaner.

Dax wing during rotation

Minor issue - when screen is rotated mid-transition, the wing is gone on the resulting input selection screen.

I'll take a look at this one separately.

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5ef9d9e. Configure here.

mikescamell and others added 3 commits April 13, 2026 10:15
…T_SCREEN

The left wing was invisible after rotating mid-transition to the input
screen. Two issues: the landscape layout anchored the wing to the parent
bottom (behind the dialog) instead of below it, and the Lottie view
needed its min/max progress set and a parent requestLayout() to render
after going from GONE to VISIBLE inside the landscape ScrollView.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mand

Remove dispatchers.io() from INPUT_SCREEN branch in onPrimaryCtaClicked
so both Finish and SkipDialogAnimation dispatch on Main, eliminating the
race where DROP_OLDEST could discard Finish if a background tap arrived
while the IO-dispatched coroutine was still running.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@LukasPaczos LukasPaczos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢

@mikescamell mikescamell merged commit cf2dbc5 into develop Apr 13, 2026
15 checks passed
@mikescamell mikescamell deleted the feature/mike/onboading-brand-design-updates/ai-chat-toggle branch April 13, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants