Files
obsidian-vault/work/wiki/apple-browsers/chat-path-onboarding-tracker-blocking.md
T

11 KiB

iOS: Chat-Path Onboarding — Tracker Blocking Demo

Asana: Ship Review task
Branch stack: demo-tracker-blocking-onboardingui-polishchat-path-dialog-polishsr-feedbackuti-flow
Due: 2026-05-15

Overview

A/B experiment adding a tracker-blocking demo step to onboarding for users who pick Duck.ai (chat-path). After the Fire Onboarding step, users see:

  1. "Try visiting a site!" dialog (address bar + toolbar hidden)
  2. Trackers-blocked dialog after a site loads
  3. "You've got this!" EOJ completion dialog
  4. Subscription upsell ("Oh, before I forget…")

Cohorts: Treatment A (address bar top) · Treatment B (address bar bottom) · Control (no change)
Audience: US only · iPhone only · Users who chose Duck.ai in onboarding, in A or B cohort


PR Stack

PR Branch Status
#4544 demo-tracker-blocking-onboarding Open — needs fixes
#4591 demo-tracker-blocking-onboarding-ui-polish Approved — needs rebase
#4664 demo-tracker-blocking-onboarding-chat-path-dialog-polish Open — needs fix
#4668 demo-tracker-blocking-onboarding-uti-flow Open — needs fix
#4855 demo-tracker-blocking-onboarding-sr-feedback Open — needs fixes

Required Changes (not yet done)

PR #4544 — main chat-path branch

  • Danger CI: @UserDefaultsWrapper(key: .daxChatPathVisitSiteSeen) — key is only defined as an enum case; no @UserDefaultsWrapper usage remains. Already resolved.
  • Mock refactor (alessandroboron): MockDaxDialogsSettings.chatPathPhase is already a plain stored property (var chatPathPhase: DaxDialogs.ChatPathPhase = .none), directly settable in tests. Already resolved.
  • Sync restore exclusion: Exclude users who selected "Restore My Stuff" (sync restore) from experiment enrollment.
    • Why it breaks: When sync data is restored, the synced homeScreenMessagesSeen > 0 value from the user's prior device triggers DaxDialogsOnboardingMigrator.migrateFromOldToNewOnboarding() to set isDismissed = true, which kills all contextual Dax dialogs (including the chat-path visit-site and trackers-blocked steps).
    • Current state: The returning-user guard in insertExperimentStepIfNeeded() (OnboardingIntroViewModel.swift:451) is commented out: // guard case .introDialog(isReturningUser: false) = introSteps.first,. Sync-restore users have isReturningUser: true (via OnboardingManager.stepsForCurrentFlow()) so uncommenting this guard is sufficient to exclude them.
    • Action: In OnboardingIntroViewModel.insertExperimentStepIfNeeded(), uncomment the line // guard case .introDialog(isReturningUser: false) = introSteps.first,. This excludes all returning users (sync restore + returning installs) from the experiment.
  • Pixels: Coordinate with rachelmcr (PR #4687).
    • Current state: Two new pixel events m_onboarding_chat-path_try-visit-site_unique and m_onboarding_chat-path_trackers-blocked_unique were added in PixelEvent.swift but have no .json5 definition in iOS/PixelDefinitions/.
    • Preferred approach: PR #4687 adds a variant parameter to existing onboarding pixels. If it merges first, the chat-path steps can use the existing pixels with variant: "chat-path" instead of new event names — remove the two new pixel event definitions from PixelEvent.swift and use the variant approach.
    • Fallback: If #4687 doesn't merge in time, add entries for both new pixels to iOS/PixelDefinitions/pixels/definitions/onboarding.json5.

PR #4664 — chat-path dialog polish

  • Title scope (alessandroboron): Fixed in commit c15122a1b0RebrandedNewTabDaxDialogFactory.createSubsequentDialog now correctly uses isChatPath ? onboardingTryASiteTitle : onboardingTryASiteNTPTitle. Already resolved.

PR #4668 — UTI (Unified Toggle Input) onboarding flow fixes

Fixes to make the chat-path onboarding dialogs work correctly when the Unified Toggle Input is active:

  • Dialog position: anchor to coordinator.viewController.view.bottomAnchor (UTI bar bottom), embed in unifiedInputContentContainer.
  • Dialog not dismissed on navigation: viewWillDisappear removes hosting controller parented to MainViewController.
  • UTI bar staying active after URL submission: preserveOmnibarSession now checks !tab.isLoading.
  • "You've got this" completion dialog path in UTI mode: showDuckAIOnboardingCompletionDialogInUTI.
  • UTI bar staying active after completion dismiss: call dismissUnifiedToggleInputToOmnibar directly.
  • Stale browsing-dialog flags: setAsChatFirstPath now resets all browsing-dialog flags to prevent chatPathPhase jumping to .trackerToEOJ from stale state.
  • Hardcoded UTI override (aataraxiaa): FeatureFlag.swift .unifiedToggleInput case has defaultValue: .enabled hardcoded (forces UTI on for testing). The original config is commented out on the next line.
    • Action: Revert to Config(source: .remoteReleasable(.subfeature(AIChatSubfeature.unifiedToggleInput))) — remove defaultValue: .enabled.
    • Note: OnboardingIntroViewModel.resolveDuckAIQueryExperimentCohortID() also has return .treatmentA hardcoded before the real flag logic. Both must be reverted before merge (the resolveDuckAIQueryExperimentCohortID fix is tracked under PR #4544 returning-user guard work above).

PR #4591 — ui-polish

  • Approved. Rebase on current base.

PR #4855 — sr-feedback

  • Hide toolbar + address bar: Replace current approach (separate setNavigationBarHidden(true) + setChatPathVisitSiteControlsLocked(true)) with chromeDelegate?.setBarsHidden(true).
    • Current code: NewTabPageViewController.swift:472-478 hides the nav bar and locks toolbar controls separately.
    • Requested approach (Costas + Alessandro): chromeDelegate?.setBarsHidden(true/false) hides both address bar and toolbar in one call, which is simpler and matches how other parts of the app hide chrome.
    • Action: In showNextDaxDialogNew (the visit-site spec == .subsequent block): replace setNavigationBarHidden(true) + setChatPathVisitSiteControlsLocked(true) with chromeDelegate?.setBarsHidden(true). In dismiss() and dismissHostingController: replace setNavigationBarHidden(false) + setChatPathVisitSiteControlsLocked(false) with chromeDelegate?.setBarsHidden(false).
  • Use delegate: NewTabPageViewController casts to (parent as? MainViewController)? in several places (e.g. lines 280, 473, 475, 510) to call setChatPathVisitSiteControlsLocked and check currentTab?.isLoading.
    • Action: Add func newTabPageControllerWillShowChatPathVisitSiteDialog() and func newTabPageControllerDidDismissChatPathVisitSiteDialog() (or equivalent) to NewTabPageControllerDelegate, implement them in MainViewController, and route the locking/bar-hiding calls through the delegate instead of casting.
    • Note: Once bars are hidden via setBarsHidden (item above), the setChatPathVisitSiteControlsLocked call may be removed entirely, simplifying the delegate surface.
  • Loading guard placement: NewTabPageViewController.swift:473: guard (parent as? MainViewController)?.currentTab?.isLoading != true else { return } exits early and leaves didHideBarsForChatPathVisitSiteDialog unset, resulting in an orphaned lock and a blank NTP if a tab happens to be loading.
    • Action: Fold the condition into the outer if block: if spec == .subsequent, chatPathPhase == .visitSite, (parent as? MainViewController)?.currentTab?.isLoading != true { ... } so the whole block (including didHideBarsForChatPathVisitSiteDialog = true) is skipped atomically.
  • Revert Search Experience screen copy: UserText.swift:2374 searchAndDuckAIOption value is currently "Toggle between\nSearch and Ask AI" — needs reverting to "Toggle between\nSearch and Duck.ai".
    • Why: The "Ask AI" rename (GJ8) applies only to the DuckAIQuery experiment toggle, which now uses the dedicated DuckAIQueryExperiment.toggleAILabel = "Ask.ai" string. The Search Experience screen in standard onboarding must keep "Duck.ai".

Privacy Config / Experiment Setup (TBD before shipping)

  • Rename experiment in privacy config — create new experiment name, disable old one.
  • Set "targets": [{ "localeLanguage": "en", "localeCountry": "US" }] in the experiment payload.
  • Inform Pete in O-N Live Onboarding Experiment Details once experiment name is confirmed.

Resolved / Dismissed

  • GJ6c — No longer required. Address bar hidden for visit-site step (GJ3), so user can't trigger a search at that point. Costas confirmed a search after the trackers-blocked dialog is acceptable.
  • GJ3 — Hide address bar + toolbar for "Try visiting a site" step (chat-path only).
  • GJ2 — X button removed from chat-path dialogs (PR #4664).
  • GJ4 — Copy updated to "Next, try visiting a site!" after AI.
  • GJ8 — "Duck.ai" → "Ask.ai" in DuckAIQuery toggle (onboarding only, not standard toggle). DuckAIQueryExperiment.toggleAILabel = "Ask.ai".
  • "Try fire tabs" flash — Suppressed via isStillOnboarding() in DaxDialogs for chat-path users.
  • GJ7 (search-only path breakage) — Returning users guarded from experiment enrollment.

Key Files

File Purpose
iOS/DuckDuckGo/DaxDialogs.swift Core onboarding dialog logic, chat-path phase state machine
iOS/DuckDuckGo/OnboardingFlow/LinearOnboarding/OnboardingIntroViewModel.swift Experiment enrollment, step injection
iOS/DuckDuckGo/NewTabPageViewController.swift Visit-site dialog presentation, toolbar hiding
iOS/DuckDuckGo/MainViewController+DuckAIExperiment.swift Chat-path completion, EOJ dialog trigger
iOS/Core/FeatureFlag.swift onboardingDuckAIQueryExperiment feature flag + cohorts
iOS/DuckDuckGo/UserText.swift Onboarding copy strings (DuckAIQueryExperiment, SearchExperience)
iOS/DuckDuckGo/OnboardingFlow/LinearOnboarding/OnboardingView+DuckAIExperimentSearchContent.swift DuckAIQuery toggle + experiment UI
iOS/DuckDuckGo/OnboardingFlow/ContextualOnboarding/Rebranding/RebrandedNewTabDaxDialogFactory.swift Chat-path dialog creation

Copy Changes Summary

Location Old New Status
DuckAIQueryExperiment.toggleAILabel (onboarding toggle) Duck.ai Ask.ai Done
searchAndDuckAIOption (Search Experience screen) Toggle between\nSearch and Duck.ai (should remain Duck.ai) Needs revert

Experiment Config

{
  "experimentName": "TBD",
  "targets": [{ "localeLanguage": "en", "localeCountry": "US" }],
  "cohorts": ["control", "treatmentA", "treatmentB"]
}

Feature flag: FeatureFlag.onboardingDuckAIQueryExperiment
Cohort type: FeatureFlag.DuckAIQueryExperimentCohort (.control, .treatmentA, .treatmentB)