diff --git a/work/projects/ios-onboarding-duck-ai-query-experiment-overrides.md b/work/projects/ios-onboarding-duck-ai-query-experiment-overrides.md new file mode 100644 index 00000000..7c7114a3 --- /dev/null +++ b/work/projects/ios-onboarding-duck-ai-query-experiment-overrides.md @@ -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"`