--- 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 .renderable(.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"`