Task/Issue URL:
https://app.asana.com/1/137249556945/project/1208671518894266/task/1216196262697685?focus=true
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/project/1208671518894266/task/1216340049398701?focus=true
### Description
Two coupled changes in the AI onboarding flows:
- Enable the toggle just-in-time, right before the End CTA is shown.
This decouples "toggle is live" from `ESTABLISHED`.
- Require the `End` CTA again, so `ESTABLISHED` fires when it's
dismissed, just like the base flow.
### Steps to test this PR
_setup_
- [x] Filter logcat by `package:mine appstage`
_search path_
- [x] Clean install the app
- [x] On input preview demo screen **select search** and submit a query
- [x] Verify `AppStage` transitions to `DAX_ONBOARDING`
- [x] On **try-a-site** step tap the address bar, verify input **toggle
is not visible**
- [x] Continue until the last 'You've got it!' CTA
- [x] Verify address bar is **focused and toggle is not visible**
- [x] Verify `AppStage` **did not** transitions to `ESTABLISHED`
- [x] Dismiss the CTA, verify address bar loses focus and keyboard
collapses
- [x] Verify `AppStage` **did** transitions to `ESTABLISHED`
- [x] Tap the address bar again, verify the **toggle is visible**
_chat path_
- [x] Clean install the app
- [x] On input preview demo screen **select Duck.ai** and submit a
prompt
- [x] Continue until the last 'You've got it!' CTA
- [x] Verify `AppStage` **did not** transitions to `ESTABLISHED`
- [x] Verify address bar is **focused and toggle is visible**
- [x] Dismiss the CTA, verify address bar loses focus and keyboard
collapses
- [x] Verify `AppStage` **did** transitions to `ESTABLISHED`
- [x] Tap the address bar again, verify the **toggle is visible**
_search-only path_
- [x] Clean install the app
- [x] On input selection screen pick **search-only**
- [x] Verify `AppStage` transitions to `DAX_ONBOARDING`
- [x] On **try-a-search** verify address bar is focused and **toggle is
not visible**
- [x] Continue until the last 'You've got it!' CTA
- [x] Verify address bar is **focused and toggle is not visible**
- [x] Verify `AppStage` **did not** transitions to `ESTABLISHED`
- [x] Dismiss the CTA, verify address bar loses focus and keyboard
collapses
- [x] Verify `AppStage` **did** transitions to `ESTABLISHED`
- [x] Tap the address bar again, verify the **toggle is not visible**
_subscriptions patch_
- [x] Apply this patch:
```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 a559ebb59b..bdfe15b100 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
@@ -139,7 +139,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()
```
_search path with subscription_
- [x] Clean install the app
- [x] On input preview demo screen **select search** and submit a query
- [x] Verify `AppStage` transitions to `DAX_ONBOARDING`
- [x] On **try-a-site** step tap the address bar, verify input **toggle
is not visible**
- [x] Continue until the last 'You've got it!' CTA
- [x] Verify address bar is **focused and toggle is not visible**
- [x] Verify `AppStage` **did not** transitions to `ESTABLISHED`
- [x] Dismiss the CTA, verify address bar loses focus and keyboard
collapses
- [x] Verify `AppStage` **did not** transitions to `ESTABLISHED`
- [x] Dismiss subscription promo.
- [x] Verify `AppStage` **did** transitions to `ESTABLISHED`
- [x] Tap the address bar again, verify the **toggle is visible**
_chat path with subscription_
- [x] Clean install the app
- [x] On input preview demo screen **select Duck.ai** and submit a
prompt
- [x] Continue until the last 'You've got it!' CTA
- [x] Verify `AppStage` **did not** transitions to `ESTABLISHED`
- [x] Verify address bar is **focused and toggle is visible**
- [x] Dismiss the CTA, verify address bar loses focus and keyboard
collapses
- [x] Verify `AppStage` **did not** transitions to `ESTABLISHED`
- [x] Dismiss subscription promo.
- [x] Verify `AppStage` **did** transitions to `ESTABLISHED`
- [x] Tap the address bar again, verify the **toggle is visible**
_custom AI onboarding patch_
- [x] Additionally apply this patch:
```diff
diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/CustomAiOnboardingStore.kt b/app/src/main/java/com/duckduckgo/app/onboarding/CustomAiOnboardingStore.kt
index 4f15d4c50e..1ab5fa5845 100644
--- a/app/src/main/java/com/duckduckgo/app/onboarding/CustomAiOnboardingStore.kt
+++ b/app/src/main/java/com/duckduckgo/app/onboarding/CustomAiOnboardingStore.kt
@@ -101,6 +101,7 @@ class CustomAiOnboardingStoreImpl @Inject constructor(
}
override suspend fun resolve() = resolveMutex.withLock {
+ return@withLock true
withContext(dispatcherProvider.io()) {
// Ensure the install referrer (and therefore the processing function) has resolved before reading.
withTimeoutOrNull(MAX_REFERRER_WAIT_TIME_MS) { referrerStateListener.get().waitForReferrerCode() }
@@ -118,6 +119,7 @@ class CustomAiOnboardingStoreImpl @Inject constructor(
}
override suspend fun isEnabled(): Boolean = resolveMutex.withLock {
+ return@withLock true
withContext(dispatcherProvider.io()) {
preferences.getBoolean(PREFS_KEY_ENABLED, false)
}
```
_custom AI path with subscription_
- [x] Clean install the app
- [x] Continue until the last 'You've got it!' CTA
- [x] Verify `AppStage` **did not** transitions to `ESTABLISHED`
- [x] Verify address bar is **focused and toggle is visible**
- [x] Dismiss the CTA, verify address bar loses focus and keyboard
collapses
- [x] Verify `AppStage` **did not** transitions to `ESTABLISHED`
- [x] Dismiss subscription promo.
- [x] Verify `AppStage` **did** transitions to `ESTABLISHED`
- [x] Tap the address bar again, verify the **toggle is visible**
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes when AppStage leaves DAX_ONBOARDING and when the input toggle
is applied—user-visible onboarding ordering—but behavior is heavily
covered by new unit tests and limited to onboarding/CTA paths.
>
> **Overview**
> **Re-aligns Duck.ai onboarding** so the input-screen toggle turns on
just before the “You've got it!” End CTA, and **`DAX_ONBOARDING` only
completes after that CTA is dismissed** (matching search onboarding),
including when Privacy Pro is still pending.
>
> In **`CtaViewModel`**, Duck.ai **required CTAs** are now fire → end →
optional Privacy Pro. **`duckChat.setInputScreenUserSetting(true)`**
runs via `setInputToggleStateForDuckAiEndCta()` when the End CTA is
prepared (legacy input screen) or shown on home (native input).
**`completeStageIfDaxOnboardingCompleted()`** no longer runs when the
End bubble is merely shown; it runs on **`onDuckAiEndCtaInteraction`**
after dismiss/OK. Subscription promo gating is unified on
**`daxOnboardingActive()`**, with **`areBubbleDaxDialogsCompleted()`**
treating Privacy Pro as the last bubble when subscriptions are
available.
>
> **`OnboardingInputScreenSelectionObserver`** ignores Duck.ai flows so
the just-in-time toggle is not treated as a user override or re-applied
at **`ESTABLISHED`**. **`RealDuckAiOnboardingDemo`** exposes
**`PRE_DISMISSED_CTAS`** for tests and demo arming.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
b09cce6a5ded660de078f83a865f9d0742e41946. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->