Files
obsidian-vault/work/projects/ios-onboarding-duck-ai-query-experiment-overrides.md
T

5.7 KiB

tags, created, project
tags created project
ios
onboarding
duck-ai
override
debug
2026-06-02 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.

// 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.

// TODO: OVERRIDE - Remove before shipping.
if !introSteps.contains(.duckAIQuerySelection) {
    introSteps.append(.duckAIQuerySelection)
}
currentIntroStep = .duckAIQuerySelection

b) resolveDuckAIQueryExperimentCohortID() — force Treatment A cohort

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.

// Production (commented out):
// guard case .introDialog(isReturningUser: true) = introSteps.first,
// Override:
guard case .renderable(.introDialog(_)) = introSteps.first,

3. iOS/DuckDuckGo/MainViewController+DuckAIExperiment.swiftisDuckAIFireFlowEnabled

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.

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.

// 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"