[2026-06-02] eagle: work/projects/ios-onboarding-duck-ai-query-experiment-overrides.md

This commit is contained in:
Alexey Martemyanov
2026-06-02 18:08:02 +06:00
parent 0280f518d8
commit 70c82ffc09
@@ -0,0 +1,131 @@
---
tags:
- ios
- onboarding
- duck-ai
- override
- debug
created: '2026-06-02'
project: address-ios-onboarding-issues
---
# iOS Onboarding — Duck.AI Query Experiment Debug Overrides
Temporary overrides to force the onboarding flow to always reset and start at the duck.ai query entry/suggestion-selection screen. All changes are marked `TODO: OVERRIDE - Remove before shipping`.
## Files changed
### 1. `iOS/DuckDuckGo/MainViewController.swift` — init (before `super.init`)
Resets all persisted onboarding flags on every launch so the linear onboarding always triggers from scratch.
```swift
// TODO: OVERRIDE - Remove before shipping.
tutorialSettings.hasSeenOnboarding = false
tutorialSettings.hasSkippedOnboarding = false
tutorialSettings.onboardingFlowType = nil // re-evaluates as .default (required for duck.ai query experiment)
let _debugDaxSettings = DefaultDaxDialogsSettings()
_debugDaxSettings.isDismissed = false
_debugDaxSettings.tryAnonymousSearchShown = false
_debugDaxSettings.tryVisitASiteShown = false
_debugDaxSettings.browsingAfterSearchShown = false
_debugDaxSettings.browsingWithTrackersShown = false
_debugDaxSettings.browsingWithoutTrackersShown = false
_debugDaxSettings.browsingMajorTrackingSiteShown = false
_debugDaxSettings.fireButtonEducationShownOrExpired = false
_debugDaxSettings.fireMessageExperimentShown = false
_debugDaxSettings.fireButtonPulseDateShown = nil
_debugDaxSettings.privacyButtonPulseShown = false
_debugDaxSettings.browsingFinalDialogShown = false
_debugDaxSettings.subscriptionPromotionDialogShown = false
_debugDaxSettings.chatPathVisitSiteSeen = false
_debugDaxSettings.isChatFirstPath = false
// Clear any resume checkpoint from a previous partial onboarding run.
// Uses UserDefaults.app.keyedStoring() (concrete type) `any KeyedStoring` existential
// can't be passed directly in the pre-super.init phase.
OnboardingResumeCheckpointStore.clearAll(in: UserDefaults.app.keyedStoring())
```
**Why `onboardingFlowType = nil`:** The duck.ai query experiment only runs on the `.default` flow. If `onboardingFlowType` is left as `.duckAI` from a previous session the experiment step never inserts.
**Why `UserDefaults.app.keyedStoring()` instead of `onboardingResumeStepStore`:** Swift can't resolve `any KeyedStoring` existential subscripts in the pre-`super.init` phase. `keyedStoring()` returns `some KeyedStoring` (opaque concrete type) which works. Same backing store.
---
### 2. `iOS/DuckDuckGo/OnboardingFlow/LinearOnboarding/OnboardingIntroViewModel.swift`
#### a) `init` — jump directly to the duck.ai query selection step
Inserts `.duckAIQuerySelection` at the end of `introSteps` and sets it as `currentIntroStep`. When `onAppear()` fires it calls `makeInitialViewState()` which renders this step immediately, skipping all preceding intro screens.
Appending at the **end** is critical: after the user selects a query, `makeNextViewState()` finds no further step and calls `completeOnboardingIntro()`, which opens duck.ai as expected. Inserting at index 1 (near the start) would advance to the next step (e.g. browser comparison screen) instead.
```swift
// TODO: OVERRIDE - Remove before shipping.
if !introSteps.contains(.duckAIQuerySelection) {
introSteps.append(.duckAIQuerySelection)
}
currentIntroStep = .duckAIQuerySelection
```
#### b) `resolveDuckAIQueryExperimentCohortID()` — force Treatment A cohort
```swift
func resolveDuckAIQueryExperimentCohortID() -> FeatureFlag.DuckAIQueryExperimentCohort? {
// TODO: OVERRIDE - Remove before shipping.
return .treatmentA
// production code below (unreachable)
guard onboardingManager.currentOnboardingFlow == .default && featureFlagger.isFeatureOn(...) else { return nil }
return featureFlagger.resolveCohort(...)
}
```
#### c) `insertExperimentStepIfNeeded()` — relax isReturningUser guard
The production guard requires `isReturningUser: true`. Since we reset `hasSeenOnboarding = false` the device looks like a new user, so the guard would fail. Changed to match any `introDialog` step.
```swift
// Production (commented out):
// guard case .introDialog(isReturningUser: true) = introSteps.first,
// Override:
guard case .introDialog = introSteps.first,
```
---
### 3. `iOS/DuckDuckGo/MainViewController+DuckAIExperiment.swift` — `isDuckAIFireFlowEnabled`
The production computed property returns `false` when the feature flag is off (which it is in debug builds) and `onboardingFlowType` is `.default`. This causes the entire fire-onboarding path (controls lock, fire dialog, completion dialog) to be skipped.
```swift
var isDuckAIFireFlowEnabled: Bool {
// TODO: OVERRIDE - Remove before shipping.
return true
// production:
// return featureFlagger.isFeatureOn(.onboardingDuckAIQueryTrackersDemoExperiment)
// || onboardingManager.currentOnboardingFlow == .duckAI
}
```
---
### 4. `SharedPackages/Onboarding/Sources/Onboarding/OnboardingSuggestedSitesProvider.swift`
Replaced default suggested site for testing.
```swift
// TODO: OVERRIDE - Remove before shipping.
default: site = "news.ru" // was "ESPN.com"
```
---
## Removing overrides (checklist)
- [ ] `MainViewController.swift` init — delete the 30-line flag-reset block
- [ ] `OnboardingIntroViewModel.swift` init — delete the 4-line step-jump block
- [ ] `OnboardingIntroViewModel.swift` `resolveDuckAIQueryExperimentCohortID` — delete `return .treatmentB` and the swiftlint comment
- [ ] `OnboardingIntroViewModel.swift` `insertExperimentStepIfNeeded` — restore `case .introDialog(isReturningUser: true)` guard, delete override comment
- [ ] `MainViewController+DuckAIExperiment.swift` `isDuckAIFireFlowEnabled` — delete `return true` and the swiftlint comment
- [ ] `OnboardingSuggestedSitesProvider.swift` — restore `default: site = "ESPN.com"`