Skip to content

Add custom AI onboarding flow#8852

Merged
LukasPaczos merged 3 commits into
developfrom
feature/lpaczos/custom-ai-onboarding/integrate-flow
Jun 18, 2026
Merged

Add custom AI onboarding flow#8852
LukasPaczos merged 3 commits into
developfrom
feature/lpaczos/custom-ai-onboarding/integrate-flow

Conversation

@LukasPaczos

@LukasPaczos LukasPaczos commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Task/Issue URL: https://app.asana.com/1/137249556945/project/1208671518894266/task/1215636599294592?focus=true
Tech Design URL (if applicable): https://app.asana.com/1/137249556945/project/481882893211075/task/1215717267206369?focus=true

Description

Integrates the custom-AI variant of new-user onboarding on top of the LinearOnboardingOrchestrator.

  • Adds buildCustomAiPlan: intro (with Duck.ai animation) → notifications → welcome → AI comparison chart → chat-only input preview → in-browser Duck.ai demo → comparison chart → set-default → address bar.
  • Adds a BrowserActivity-hosted onboarding step (NewUserBrowserActivityStep + NewUserBrowserOnboardingViewModel) so the orchestrator can hand off OnboardingActivityBrowserActivity for the live Duck.ai demo and return.
  • Extracts DuckAiOnboardingDemo to arm the demo (mark the flow + silence the standard DAX CTAs) identically from the legacy onboarding-done path and the new step.
  • Derives the step-indicator "N of M" from each step's position in the plan (showsStepIndicator), and enables custom-AI copy variants for the relevant dialogs. The default flow retains 3 indicated steps while the custom-AI flow has 4.
  • Always finishes the custom AI flow on an open Duck.ai input tab.

Compatibility notes:

  1. isCustomAiOnboardingFlow() is still a stub (returns false) — the flow is dark until it is wired to a real flag.
  2. The flow currently only works with native input disabled and weaves in some old-design onboarding CTAs, both will be resolved in a dependent project.

Steps to test this PR

Apply below diff:

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 d9cf899bad..e941d7e28f 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
@@ -131,7 +131,7 @@ class CtaViewModel @Inject constructor(
     }
 
     private suspend fun isSubscriptionCtaAvailable(): Boolean =
-        subscriptions.isEligible() && hasNoSubscription() && extendedOnboardingFeatureToggles.privacyProCta().isEnabled()
+        true // subscriptions.isEligible() && hasNoSubscription() && extendedOnboardingFeatureToggles.privacyProCta().isEnabled()
 
     private suspend fun isBrandDesignUpdateEnabled(): Boolean = withContext(dispatchers.io()) {
         onboardingBrandDesignUpdateToggles.brandDesignUpdate().isEnabled()
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 19f4e74c11..ad958e68ce 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
@@ -222,7 +222,7 @@ class OnboardingStoreImpl @Inject constructor(
     }
 
     override fun isCustomAiOnboardingFlow(): Boolean {
-        return false
+        return true
     }
 
     companion object {
diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/feature/DuckChatFeature.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/feature/DuckChatFeature.kt
index 356b512149..f0df1e15b5 100644
--- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/feature/DuckChatFeature.kt
+++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/feature/DuckChatFeature.kt
@@ -141,7 +141,8 @@ interface DuckChatFeature {
     /**
      * @return `true` when the Native Input Field should be used instead of the web-based input.
      */
-    @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
+    // @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
+    @Toggle.DefaultValue(DefaultFeatureValue.FALSE)
     fun nativeInputField(): Toggle
 
     /**

Base flow

  • Uninstall the app (just clearing storage is not enough)
  • Install and open the app
  • Go through the flow and ensure we're hitting all the screens from the designs
  • Verify the flow finishes on an open chat tab

Skip flow

  • Uninstall the app (just clearing storage is not enough)
  • Install and open the app
  • Click "I've been here before" and "Start AI Chat"
  • Verify the flow finishes on an open chat tab

Additional checks

  • Try rotating the screen at any point in the flow and verify there are no crashes or mangled transitions, especially when going across Onboarding <-> Browser Activity boundaries
  • Try minimizing the app at any step of the flow, on resume it shouldn't progress automatically
  • Try killing the app at any step of the flow, reopening the app should restart the flow from scratch and all the steps should still be navigable

UI changes

Screen_recording_20260612_195956.mp4

Note

Medium Risk
Large, user-visible onboarding changes with Activity handoffs and input-mode behavior, but the custom path stays off until isCustomAiOnboardingFlow() is wired beyond the current stub.

Overview
When isCustomAiOnboardingFlow() is true, buildRootPlan selects a custom AI plan (Duck.ai intro, AI comparison chart, chat-only input preview, in-browser duck_ai_demo, then browser comparison/default/address steps) instead of the default sequence. Sync restore is skipped on that path, and plan completion/skip dismisses the Duck.ai fire CTA and arms a one-shot signal so the next auto-launched input opens on the Duck.ai (chat) tab via initialInputMode / LaunchInputScreen.launchOnChat.

BrowserActivity now participates in onboarding through NewUserBrowserOnboardingViewModel: it hands off to OnboardingActivity, opens Duck.ai for the demo, and reports DuckAiFireCompleted after the onboarding fire flow. Duck.ai launch logic is centralized in launchDuckAi. DuckAiOnboardingDemo.arm() replaces duplicated “set flow + dismiss DAX CTAs” logic for both legacy onboarding-done and the orchestrator demo step. Custom AI defers early dismissal of the Duck.ai fire onboarding CTA until the orchestrator finishes (kill/resume safety).

The welcome UI gains plan-derived step indicators (showsStepIndicator), custom-AI copy, chat-only preview (toggle hidden), and fresh-entry handling when returning from BrowserActivity (comparison chart fade-in vs morph). CustomDuckAiOnboardingFeature.introAnimation() is removed in favor of plan-driven intro. OnboardingStoreImpl moves to SharedPreferencesProvider. CtaViewModel completes DAX stage when the root linear plan completes.

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

@LukasPaczos

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
@LukasPaczos LukasPaczos force-pushed the feature/lpaczos/custom-ai-onboarding/integrate-flow branch 2 times, most recently from 11550b6 to 7515b63 Compare June 12, 2026 18:04
@LukasPaczos LukasPaczos marked this pull request as ready for review June 12, 2026 18:05
@LukasPaczos

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt Outdated
@LukasPaczos LukasPaczos force-pushed the feature/lpaczos/brand-design-view-model-orchestrator branch from 3db4691 to 396d5c0 Compare June 15, 2026 11:17
@LukasPaczos LukasPaczos force-pushed the feature/lpaczos/custom-ai-onboarding/integrate-flow branch from 7515b63 to 242789f Compare June 15, 2026 11:44
@LukasPaczos LukasPaczos marked this pull request as draft June 15, 2026 11:44
@LukasPaczos LukasPaczos force-pushed the feature/lpaczos/brand-design-view-model-orchestrator branch from 396d5c0 to d92688f Compare June 15, 2026 12:08
@LukasPaczos LukasPaczos force-pushed the feature/lpaczos/custom-ai-onboarding/integrate-flow branch 4 times, most recently from e66917e to 28a032e Compare June 15, 2026 17:41
@LukasPaczos

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

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 28a032e. Configure here.

@LukasPaczos LukasPaczos marked this pull request as ready for review June 15, 2026 18:00
@LukasPaczos LukasPaczos force-pushed the feature/lpaczos/custom-ai-onboarding/integrate-flow branch from 28a032e to 3568305 Compare June 15, 2026 18:48
@LukasPaczos LukasPaczos force-pushed the feature/lpaczos/brand-design-view-model-orchestrator branch from 0b3d336 to aaa4c4f Compare June 15, 2026 18:54
@LukasPaczos LukasPaczos force-pushed the feature/lpaczos/custom-ai-onboarding/integrate-flow branch from 3568305 to b0f79ef Compare June 15, 2026 18:55
Base automatically changed from feature/lpaczos/brand-design-view-model-orchestrator to develop June 17, 2026 07:59
@LukasPaczos LukasPaczos force-pushed the feature/lpaczos/custom-ai-onboarding/integrate-flow branch from b0f79ef to d583a98 Compare June 17, 2026 08:03

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

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.

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 d583a98. Configure here.

@LukasPaczos LukasPaczos force-pushed the feature/lpaczos/custom-ai-onboarding/integrate-flow branch from d583a98 to b6762cd Compare June 17, 2026 10:47

@lmac012 lmac012 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For some reason, in the "skip flow" scenario, I always end up on a search tab (instead of a chat tab). Everything else worked as expected 👍

…stom-ai-onboarding/integrate-flow

# Conflicts:
#	app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
#	app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
#	app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt
#	app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt
#	duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/inputscreen/InputScreenActivityParams.kt
#	duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/InputScreenFragment.kt
#	duckchat/duckchat-impl/src/main/res/values/donottranslate.xml
@LukasPaczos

Copy link
Copy Markdown
Contributor Author

"skip flow" scenario, I always end up on a search tab

I pushed minor tweaks to fix that yesterday, should've been on the version you tested. Either way, let's follow up separately if needed.

Screen_recording_20260618_103435.mp4

@LukasPaczos LukasPaczos merged commit 4811b30 into develop Jun 18, 2026
14 checks passed
@LukasPaczos LukasPaczos deleted the feature/lpaczos/custom-ai-onboarding/integrate-flow branch June 18, 2026 09:11
LukasPaczos added a commit that referenced this pull request Jun 22, 2026
…preconditions (#8914)

Task/Issue URL:
https://app.asana.com/1/137249556945/project/1208671518894266/task/1215753390280440?focus=true
Tech Design URL (if applicable): 

### Description

Stacked on #8852.

- **`CustomAiOnboardingStore` / `CustomAiOnboardingStoreImpl`** — a
single component that is both the referrer parser plugin (writes the
`onboarding=ai` flag on first launch) and the reader of the custom AI
onboarding decision. The decision is made once and then frozen.
`resolve()` waits for the install referrer to resolve, then returns true
only when the referral flag is set and the `customDuckAiOnboarding`,
`linearOnboardingOrchestrator` and `brandDesignUpdate` flags are all
enabled, and persists that result. `isEnabled()` is a cheap read of the
persisted decision and does not re-evaluate, so the run is chosen once
at plan build time and every later reader (onboarding pages,
end-of-journey CTAs) sees the same value rather than recomputing and
possibly diverging from the chosen plan. The two roles are split across
`CustomAiOnboardingResolver` (decide and write) and
`CustomAiOnboardingStore` (read).
- **`NewUserOnboardingPlanProvider`** — picks the custom AI plan from
`resolve()`, arms the in-context Duck.ai demo up front, and gates the
input screen preview and Duck.ai demo steps behind the
`singleTabFireDialog` flag as this feature is required for these steps
to be executed.


### Steps to test this PR
See
[this](https://app.asana.com/1/137249556945/project/1207137509162935/task/1207930443876573?focus=true)
for context on how to test referrals.

Also see
[Figma](https://www.figma.com/design/5QvJbyUBbeonblsjViDIkf/Mobile-Onboarding-AI-First?node-id=173-50969&t=w51SVY4Lvfst0gyJ-1).

- [x] Uninstall the app
- [x] Apply this patch:
```diff
diff --git a/app/build.gradle b/app/build.gradle
index 541dfe1..5f5570725f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -142,7 +142,7 @@ android {
     }
     buildTypes {
         debug {
-            applicationIdSuffix ".debug"
+//            applicationIdSuffix ".debug"
             pseudoLocalesEnabled false
             manifestPlaceholders = [
                     appIcon     : "@mipmap/ic_launcher_blue",
 }
```
- [x] Change build type to `PlayDebug`
- [x] Open this link on the test device:
https://play.google.com/store/apps/details?id=com.duckduckgo.mobile.android&referrer=utm_campaign%3Dappnosupport-atb-appnosupport%26origin%3Dfunnel_appnosupport_website
- [x] Install the app (remember to use `PlayDebug`)
- [x] Verify you **don't see** the `+ Duck.ai` on the intro animation
and a **regular onboarding flow** launches
- [x] Uninstall the app
- [x] Open this link on the test device:
https://play.google.com/store/apps/details?id=com.duckduckgo.mobile.android&referrer=utm_campaign%3Dappnosupport-atb-appnosupport%26origin%3Dfunnel_appnosupport_website%26onboarding%3Dai
- [x] Install the app (remember to use `PlayDebug`)
- [x] Verify you **don't see** the `+ Duck.ai` on the intro animation
and a **regular onboarding flow** launches.
- [x] Additionally apply this diff:
```diff
diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/CustomDuckAiOnboardingFeature.kt b/app/src/main/java/com/duckduckgo/app/onboarding/CustomDuckAiOnboardingFeature.kt
index 8ccb40d..d862242281 100644
--- a/app/src/main/java/com/duckduckgo/app/onboarding/CustomDuckAiOnboardingFeature.kt
+++ b/app/src/main/java/com/duckduckgo/app/onboarding/CustomDuckAiOnboardingFeature.kt
@@ -27,6 +27,6 @@ import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue
 )
 interface CustomDuckAiOnboardingFeature {
 
-    @Toggle.DefaultValue(DefaultFeatureValue.FALSE)
+    @Toggle.DefaultValue(DefaultFeatureValue.TRUE)
     fun self(): Toggle
```
- [x] Open this same link on the test device again:
https://play.google.com/store/apps/details?id=com.duckduckgo.mobile.android&referrer=utm_campaign%3Dappnosupport-atb-appnosupport%26origin%3Dfunnel_appnosupport_website%26onboarding%3Dai
- [x] Install the app (remember to use `PlayDebug`)
- [x] Verify you **see** the `+ Duck.ai` on the intro animation and a
**custom AI onboarding flow** launches




<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes first-launch onboarding plan selection and install-referrer
timing, with broad wiring in browser CTAs and the linear orchestrator;
mitigated by persisted frozen decisions and extensive unit tests.
> 
> **Overview**
> Introduces **`CustomAiOnboardingStore`** as the single place for the
Play Install Referrer `onboarding=ai` signal, a one-time **`resolve()`**
decision (referrer + feature toggles), and a frozen **`isEnabled()`**
read so the whole app agrees on whether the custom AI plan ran.
Custom-AI helpers are **removed from `OnboardingStore`** (no more stub
`isCustomAiOnboardingFlow()`).
> 
> **`NewUserOnboardingPlanProvider`** now branches on
**`customAiOnboardingResolver.resolve()`**, arms the in-context Duck.ai
demo at plan build, and gates the input preview / Duck.ai demo steps on
**`singleTabFireDialog`**. Finish/skip of the custom path arms a
one-shot **open input on Duck.ai** via the new store.
> 
> Browser and CTA layers switch to **`customAiOnboardingStore`** for
chat-tab launch, Privacy Pro `featurePage=duckai`, deferred fire-button
dismissal in the custom flow, and **custom copy** on brand-design
end/subscription bubbles (via an explicit `isCustomAiOnboardingFlow`
flag). **`BrandDesignUpdatePageViewModel`** loads that flag
asynchronously before orchestrator UI.
**`NewUserBrowserOnboardingViewModel`** no longer arms the demo
locally—arming stays in the plan provider.
> 
> Tests move from `OnboardingStoreImpl` to
**`CustomAiOnboardingStoreImplTest`** and update mocks across
CTA/browser/plan/orchestrator tests.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
d5dbb20. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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