11 KiB
11 KiB
iOS: Chat-Path Onboarding — Tracker Blocking Demo
Asana: Ship Review task
Branch stack:demo-tracker-blocking-onboarding→ui-polish→chat-path-dialog-polish→sr-feedback→uti-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:
- "Try visiting a site!" dialog (address bar + toolbar hidden)
- Trackers-blocked dialog after a site loads
- "You've got this!" EOJ completion dialog
- 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@UserDefaultsWrapperusage remains. Already resolved. - Mock refactor (alessandroboron):
MockDaxDialogsSettings.chatPathPhaseis 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 > 0value from the user's prior device triggersDaxDialogsOnboardingMigrator.migrateFromOldToNewOnboarding()to setisDismissed = 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 haveisReturningUser: true(viaOnboardingManager.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.
- Why it breaks: When sync data is restored, the synced
- Pixels: Coordinate with rachelmcr (PR #4687).
- Current state: Two new pixel events
m_onboarding_chat-path_try-visit-site_uniqueandm_onboarding_chat-path_trackers-blocked_uniquewere added inPixelEvent.swiftbut have no.json5definition iniOS/PixelDefinitions/. - Preferred approach: PR #4687 adds a
variantparameter to existing onboarding pixels. If it merges first, the chat-path steps can use the existing pixels withvariant: "chat-path"instead of new event names — remove the two new pixel event definitions fromPixelEvent.swiftand 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.
- Current state: Two new pixel events
PR #4664 — chat-path dialog polish
- Title scope (alessandroboron): Fixed in commit
c15122a1b0—RebrandedNewTabDaxDialogFactory.createSubsequentDialognow correctly usesisChatPath ? 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 inunifiedInputContentContainer. - Dialog not dismissed on navigation:
viewWillDisappearremoves hosting controller parented toMainViewController. - UTI bar staying active after URL submission:
preserveOmnibarSessionnow checks!tab.isLoading. - "You've got this" completion dialog path in UTI mode:
showDuckAIOnboardingCompletionDialogInUTI. - UTI bar staying active after completion dismiss: call
dismissUnifiedToggleInputToOmnibardirectly. - Stale browsing-dialog flags:
setAsChatFirstPathnow resets all browsing-dialog flags to preventchatPathPhasejumping to.trackerToEOJfrom stale state. - Hardcoded UTI override (aataraxiaa):
FeatureFlag.swift.unifiedToggleInputcase hasdefaultValue: .enabledhardcoded (forces UTI on for testing). The original config is commented out on the next line.- Action: Revert to
Config(source: .remoteReleasable(.subfeature(AIChatSubfeature.unifiedToggleInput)))— removedefaultValue: .enabled. - Note:
OnboardingIntroViewModel.resolveDuckAIQueryExperimentCohortID()also hasreturn .treatmentAhardcoded before the real flag logic. Both must be reverted before merge (theresolveDuckAIQueryExperimentCohortIDfix is tracked under PR #4544 returning-user guard work above).
- Action: Revert to
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)) withchromeDelegate?.setBarsHidden(true).- Current code:
NewTabPageViewController.swift:472-478hides 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-sitespec == .subsequentblock): replacesetNavigationBarHidden(true)+setChatPathVisitSiteControlsLocked(true)withchromeDelegate?.setBarsHidden(true). Indismiss()anddismissHostingController: replacesetNavigationBarHidden(false)+setChatPathVisitSiteControlsLocked(false)withchromeDelegate?.setBarsHidden(false).
- Current code:
- Use delegate:
NewTabPageViewControllercasts to(parent as? MainViewController)?in several places (e.g. lines 280, 473, 475, 510) to callsetChatPathVisitSiteControlsLockedand checkcurrentTab?.isLoading.- Action: Add
func newTabPageControllerWillShowChatPathVisitSiteDialog()andfunc newTabPageControllerDidDismissChatPathVisitSiteDialog()(or equivalent) toNewTabPageControllerDelegate, implement them inMainViewController, and route the locking/bar-hiding calls through the delegate instead of casting. - Note: Once bars are hidden via
setBarsHidden(item above), thesetChatPathVisitSiteControlsLockedcall may be removed entirely, simplifying the delegate surface.
- Action: Add
- Loading guard placement:
NewTabPageViewController.swift:473:guard (parent as? MainViewController)?.currentTab?.isLoading != true else { return }exits early and leavesdidHideBarsForChatPathVisitSiteDialogunset, resulting in an orphaned lock and a blank NTP if a tab happens to be loading.- Action: Fold the condition into the outer
ifblock:if spec == .subsequent, chatPathPhase == .visitSite, (parent as? MainViewController)?.currentTab?.isLoading != true { ... }so the whole block (includingdidHideBarsForChatPathVisitSiteDialog = true) is skipped atomically.
- Action: Fold the condition into the outer
- Revert Search Experience screen copy:
UserText.swift:2374searchAndDuckAIOptionvalue 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".
- Why: The "Ask AI" rename (GJ8) applies only to the DuckAIQuery experiment toggle, which now uses the dedicated
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()inDaxDialogsfor 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)