[2026-06-02] eagle: work/projects/ios-onboarding-duck-ai-query-experiment-overrides.md
This commit is contained in:
@@ -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"`
|
||||
Reference in New Issue
Block a user