Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit b50b7fa

Browse files
Onboarding Add to Dock Refactor for Intro scenario (#3538)
Task/Issue URL: https://app.asana.com/0/72649045549333/1208648960421864/f **Description**: Refactor the logic to show Add to Dock from the Onboarding Intro or the end of the contextual flow.
1 parent eb72d54 commit b50b7fa

26 files changed

+516
-75
lines changed

Core/UserDefaultsPropertyWrapper.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ public struct UserDefaultsWrapper<T> {
171171
// Debug keys
172172
case debugNewTabPageSectionsEnabledKey = "com.duckduckgo.ios.debug.newTabPageSectionsEnabled"
173173
case debugOnboardingHighlightsEnabledKey = "com.duckduckgo.ios.debug.onboardingHighlightsEnabled"
174-
case debugOnboardingAddToDockEnabledKey = "com.duckduckgo.ios.debug.onboardingAddToDockEnabled"
175174

176175
// Duck Player Pixel Experiment
177176
case duckPlayerPixelExperimentInstalled = "com.duckduckgo.ios.duckplayer.pixel.experiment.installed.v2"

DuckDuckGo.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@
713713
9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */; };
714714
9F23B8062C2BE22700950875 /* OnboardingIntroViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */; };
715715
9F23B8092C2BE9B700950875 /* MockURLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */; };
716+
9F46BEF82CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */; };
716717
9F4CC5152C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC5142C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift */; };
717718
9F4CC5172C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC5162C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift */; };
718719
9F4CC51B2C48C0C7006A96EB /* MockTabDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4CC51A2C48C0C7006A96EB /* MockTabDelegate.swift */; };
@@ -2514,6 +2515,7 @@
25142515
9F23B8022C2BCD0000950875 /* DaxDialogStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogStyles.swift; sourceTree = "<group>"; };
25152516
9F23B8052C2BE22700950875 /* OnboardingIntroViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModelTests.swift; sourceTree = "<group>"; };
25162517
9F23B8082C2BE9B700950875 /* MockURLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLOpener.swift; sourceTree = "<group>"; };
2518+
9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AddToDockContent.swift"; sourceTree = "<group>"; };
25172519
9F4CC5142C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingPresenterMock.swift; sourceTree = "<group>"; };
25182520
9F4CC5162C48B8D4006A96EB /* TabViewControllerDaxDialogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewControllerDaxDialogTests.swift; sourceTree = "<group>"; };
25192521
9F4CC51A2C48C0C7006A96EB /* MockTabDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTabDelegate.swift; sourceTree = "<group>"; };
@@ -4810,6 +4812,7 @@
48104812
9F8E0F302CCA6390001EA7C5 /* AddToDockTutorialView.swift */,
48114813
9F8E0F372CCFAA8A001EA7C5 /* AddToDockPromoView.swift */,
48124814
9F8E0F3C2CCFD071001EA7C5 /* AddToDockPromoViewModel.swift */,
4815+
9F46BEF72CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift */,
48134816
);
48144817
path = AddToDock;
48154818
sourceTree = "<group>";
@@ -7408,6 +7411,7 @@
74087411
F4E1936625AF722F001D2666 /* HighlightCutOutView.swift in Sources */,
74097412
1E162605296840D80004127F /* Triangle.swift in Sources */,
74107413
6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */,
7414+
9F46BEF82CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift in Sources */,
74117415
B609D5522862EAFF0088CAC2 /* InlineWKDownloadDelegate.swift in Sources */,
74127416
BDFF03222BA3D8E200F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */,
74137417
B652DEFD287BE67400C12A9C /* UserScripts.swift in Sources */,

DuckDuckGo/AppSettings.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,5 @@ protocol AppSettings: AnyObject, AppDebugSettings {
8787

8888
protocol AppDebugSettings {
8989
var onboardingHighlightsEnabled: Bool { get set }
90-
var onboardingAddToDockEnabled: Bool { get set }
90+
var onboardingAddToDockState: OnboardingAddToDockState { get set }
9191
}

DuckDuckGo/AppUserDefaults.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public class AppUserDefaults: AppSettings {
8484
private struct DebugKeys {
8585
static let inspectableWebViewsEnabledKey = "com.duckduckgo.ios.debug.inspectableWebViewsEnabled"
8686
static let autofillDebugScriptEnabledKey = "com.duckduckgo.ios.debug.autofillDebugScriptEnabled"
87+
static let onboardingAddToDockStateKey = "com.duckduckgo.ios.debug.onboardingAddToDockState"
8788
}
8889

8990
private var userDefaults: UserDefaults? {
@@ -422,8 +423,15 @@ public class AppUserDefaults: AppSettings {
422423
@UserDefaultsWrapper(key: .debugOnboardingHighlightsEnabledKey, defaultValue: false)
423424
var onboardingHighlightsEnabled: Bool
424425

425-
@UserDefaultsWrapper(key: .debugOnboardingAddToDockEnabledKey, defaultValue: false)
426-
var onboardingAddToDockEnabled: Bool
426+
var onboardingAddToDockState: OnboardingAddToDockState {
427+
get {
428+
guard let rawValue = userDefaults?.string(forKey: DebugKeys.onboardingAddToDockStateKey) else { return .disabled }
429+
return OnboardingAddToDockState(rawValue: rawValue) ?? .disabled
430+
}
431+
set {
432+
userDefaults?.set(newValue.rawValue, forKey: DebugKeys.onboardingAddToDockStateKey)
433+
}
434+
}
427435
}
428436

429437
extension AppUserDefaults: AppConfigurationFetchStatistics {

DuckDuckGo/DaxDialogs.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ protocol ContextualOnboardingLogic {
4141
var shouldShowPrivacyButtonPulse: Bool { get }
4242
var isShowingSearchSuggestions: Bool { get }
4343
var isShowingSitesSuggestions: Bool { get }
44+
var isShowingAddToDockDialog: Bool { get }
4445

4546
func setSearchMessageSeen()
4647
func setFireEducationMessageSeen()
@@ -211,6 +212,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
211212
private var settings: DaxDialogsSettings
212213
private var entityProviding: EntityProviding
213214
private let variantManager: VariantManager
215+
private let addToDockManager: OnboardingAddToDockManaging
214216

215217
private var nextHomeScreenMessageOverride: HomeScreenSpec?
216218

@@ -222,10 +224,13 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
222224
/// Use singleton accessor, this is only accessible for tests
223225
init(settings: DaxDialogsSettings = DefaultDaxDialogsSettings(),
224226
entityProviding: EntityProviding,
225-
variantManager: VariantManager = DefaultVariantManager()) {
227+
variantManager: VariantManager = DefaultVariantManager(),
228+
onboardingManager: OnboardingAddToDockManaging = OnboardingManager()
229+
) {
226230
self.settings = settings
227231
self.entityProviding = entityProviding
228232
self.variantManager = variantManager
233+
self.addToDockManager = onboardingManager
229234
}
230235

231236
private var isNewOnboarding: Bool {
@@ -276,6 +281,11 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
276281
return lastShownDaxDialogType.flatMap(BrowsingSpec.SpecType.init(rawValue:)) == .visitWebsite || currentHomeSpec == .subsequent
277282
}
278283

284+
var isShowingAddToDockDialog: Bool {
285+
guard isNewOnboarding else { return false }
286+
return currentHomeSpec == .final && addToDockManager.addToDockEnabledState == .contextual
287+
}
288+
279289
var isEnabled: Bool {
280290
// skip dax dialogs in integration tests
281291
guard ProcessInfo.processInfo.environment["DAXDIALOGS"] != "false" else { return false }
@@ -733,6 +743,7 @@ final class DaxDialogs: NewTabDialogSpecProvider, ContextualOnboardingLogic {
733743
private func clearOnboardingBrowsingData() {
734744
removeLastShownDaxDialog()
735745
removeLastVisitedOnboardingWebsite()
746+
currentHomeSpec = nil
736747
}
737748
}
738749

DuckDuckGo/MainViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2672,7 +2672,7 @@ extension MainViewController: AutoClearWorker {
26722672
// Ideally this should happen once data clearing has finished AND the animation is finished
26732673
if showNextDaxDialog {
26742674
self.newTabPageViewController?.showNextDaxDialog()
2675-
} else if KeyboardSettings().onNewTab {
2675+
} else if KeyboardSettings().onNewTab && !self.contextualOnboardingLogic.isShowingAddToDockDialog { // If we're showing the Add to Dock dialog prevent address bar to become first responder. We want to make sure the user focues on the Add to Dock instructions.
26762676
let showKeyboardAfterFireButton = DispatchWorkItem {
26772677
self.enterSearch()
26782678
}

DuckDuckGo/NewTabPageViewController.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,12 @@ extension NewTabPageViewController {
310310

311311
guard let spec = dialogProvider.nextHomeScreenMessageNew() else { return }
312312

313-
let onDismiss = {
313+
let onDismiss = { [weak self] in
314+
guard let self else { return }
314315
dialogProvider.dismiss()
315316
self.dismissHostingController(didFinishNTPOnboarding: true)
317+
// Make the address bar first responder after closing the new tab page final dialog.
318+
self.launchNewSearch()
316319
}
317320
let daxDialogView = AnyView(factory.createDaxDialog(for: spec, onDismiss: onDismiss))
318321
let hostingController = UIHostingController(rootView: daxDialogView)

DuckDuckGo/OnboardingDebugView.swift

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import SwiftUI
2222
struct OnboardingDebugView: View {
2323

2424
@StateObject private var viewModel = OnboardingDebugViewModel()
25+
@State private var isShowingResetDaxDialogsAlert = false
2526

2627
private let newOnboardingIntroStartAction: () -> Void
2728

@@ -45,8 +46,13 @@ struct OnboardingDebugView: View {
4546
}
4647

4748
Section {
48-
Toggle(
49-
isOn: $viewModel.isOnboardingAddToDockLocalFlagEnabled,
49+
Picker(
50+
selection: $viewModel.onboardingAddToDockLocalFlagState,
51+
content: {
52+
ForEach(OnboardingAddToDockState.allCases) { state in
53+
Text(verbatim: state.description).tag(state)
54+
}
55+
},
5056
label: {
5157
Text(verbatim: "Onboarding Add to Dock local setting enabled")
5258
}
@@ -57,6 +63,18 @@ struct OnboardingDebugView: View {
5763
Text(verbatim: "Requires internal user flag set to have an effect.")
5864
}
5965

66+
Section {
67+
Button(action: {
68+
viewModel.resetDaxDialogs()
69+
isShowingResetDaxDialogsAlert = true
70+
}, label: {
71+
Text(verbatim: "Reset Dax Dialogs State")
72+
})
73+
.alert(isPresented: $isShowingResetDaxDialogsAlert, content: {
74+
Alert(title: Text(verbatim: "Dax Dialogs reset"), dismissButton: .cancel())
75+
})
76+
}
77+
6078
Section {
6179
Button(action: newOnboardingIntroStartAction, label: {
6280
let onboardingType = viewModel.isOnboardingHighlightsLocalFlagEnabled ? "Highlights" : ""
@@ -74,22 +92,44 @@ final class OnboardingDebugViewModel: ObservableObject {
7492
}
7593
}
7694

77-
@Published var isOnboardingAddToDockLocalFlagEnabled: Bool {
95+
@Published var onboardingAddToDockLocalFlagState: OnboardingAddToDockState {
7896
didSet {
79-
manager.isAddToDockLocalFlagEnabled = isOnboardingAddToDockLocalFlagEnabled
97+
manager.addToDockLocalFlagState = onboardingAddToDockLocalFlagState
8098
}
8199
}
82100

83101
private let manager: OnboardingHighlightsDebugging & OnboardingAddToDockDebugging
102+
private var settings: DaxDialogsSettings
84103

85-
init(manager: OnboardingHighlightsDebugging & OnboardingAddToDockDebugging = OnboardingManager()) {
104+
init(manager: OnboardingHighlightsDebugging & OnboardingAddToDockDebugging = OnboardingManager(), settings: DaxDialogsSettings = DefaultDaxDialogsSettings()) {
86105
self.manager = manager
106+
self.settings = settings
87107
isOnboardingHighlightsLocalFlagEnabled = manager.isOnboardingHighlightsLocalFlagEnabled
88-
isOnboardingAddToDockLocalFlagEnabled = manager.isAddToDockLocalFlagEnabled
108+
onboardingAddToDockLocalFlagState = manager.addToDockLocalFlagState
89109
}
90110

111+
func resetDaxDialogs() {
112+
settings.isDismissed = false
113+
settings.homeScreenMessagesSeen = 0
114+
settings.browsingAfterSearchShown = false
115+
settings.browsingWithTrackersShown = false
116+
settings.browsingWithoutTrackersShown = false
117+
settings.browsingMajorTrackingSiteShown = false
118+
settings.fireMessageExperimentShown = false
119+
settings.fireButtonPulseDateShown = nil
120+
settings.privacyButtonPulseShown = false
121+
settings.browsingFinalDialogShown = false
122+
settings.lastVisitedOnboardingWebsiteURLPath = nil
123+
settings.lastShownContextualOnboardingDialogType = nil
124+
}
91125
}
92126

93127
#Preview {
94128
OnboardingDebugView(onNewOnboardingIntroStartAction: {})
95129
}
130+
131+
extension OnboardingAddToDockState: Identifiable {
132+
var id: OnboardingAddToDockState {
133+
self
134+
}
135+
}

DuckDuckGo/OnboardingExperiment/AddToDock/AddToDockTutorialView.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ struct AddToDockTutorialView: View {
3232

3333
private let title: String
3434
private let message: String
35+
private let cta: String
3536
private let action: () -> Void
3637

3738
@State private var animateTitle = true
@@ -44,10 +45,12 @@ struct AddToDockTutorialView: View {
4445
init(
4546
title: String,
4647
message: String,
48+
cta: String,
4749
action: @escaping () -> Void
4850
) {
4951
self.title = title
5052
self.message = message
53+
self.cta = cta
5154
self.action = action
5255
}
5356

@@ -81,7 +84,7 @@ struct AddToDockTutorialView: View {
8184
}
8285

8386
Button(action: action) {
84-
Text(UserText.AddToDockOnboarding.Buttons.dismiss)
87+
Text(cta)
8588
}
8689
.buttonStyle(PrimaryButtonStyle())
8790
.visibility(showContent ? .visible : .invisible)
@@ -110,6 +113,7 @@ struct AddToDockTutorial_Previews: PreviewProvider {
110113
AddToDockTutorialView(
111114
title: UserText.AddToDockOnboarding.Tutorial.title,
112115
message: UserText.AddToDockOnboarding.Tutorial.message,
116+
cta: UserText.AddToDockOnboarding.Buttons.dismiss,
113117
action: {}
114118
)
115119
.padding()
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//
2+
// OnboardingView+AddToDockContent.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2024 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import SwiftUI
21+
import Onboarding
22+
23+
extension OnboardingView {
24+
25+
struct AddToDockPromoContentState {
26+
var animateTitle = true
27+
var animateMessage = false
28+
var showContent = false
29+
}
30+
31+
struct AddToDockPromoContent: View {
32+
33+
@State private var showAddToDockTutorial = false
34+
35+
private var animateTitle: Binding<Bool>
36+
private var animateMessage: Binding<Bool>
37+
private var showContent: Binding<Bool>
38+
private let dismissAction: (_ fromAddToDock: Bool) -> Void
39+
40+
init(
41+
animateTitle: Binding<Bool> = .constant(true),
42+
animateMessage: Binding<Bool> = .constant(true),
43+
showContent: Binding<Bool> = .constant(false),
44+
dismissAction: @escaping (_ fromAddToDock: Bool) -> Void
45+
) {
46+
self.animateTitle = animateTitle
47+
self.animateMessage = animateMessage
48+
self.showContent = showContent
49+
self.dismissAction = dismissAction
50+
}
51+
52+
var body: some View {
53+
if showAddToDockTutorial {
54+
OnboardingAddToDockTutorialContent(cta: UserText.AddToDockOnboarding.Intro.tutorialDismissCTA) {
55+
dismissAction(true)
56+
}
57+
} else {
58+
ContextualDaxDialogContent(
59+
title: UserText.AddToDockOnboarding.Intro.title,
60+
titleFont: Font(UIFont.daxTitle3()),
61+
message: NSAttributedString(string: UserText.AddToDockOnboarding.Intro.message),
62+
messageFont: Font.system(size: 16),
63+
customView: AnyView(addToDockPromoView),
64+
customActionView: AnyView(customActionView)
65+
)
66+
}
67+
}
68+
69+
private var addToDockPromoView: some View {
70+
AddToDockPromoView()
71+
.aspectRatio(contentMode: .fit)
72+
.padding(.vertical)
73+
}
74+
75+
private var customActionView: some View {
76+
VStack {
77+
OnboardingCTAButton(
78+
title: UserText.AddToDockOnboarding.Buttons.addToDockTutorial,
79+
action: {
80+
showAddToDockTutorial = true
81+
}
82+
)
83+
84+
OnboardingCTAButton(
85+
title: UserText.AddToDockOnboarding.Intro.skipCTA,
86+
buttonStyle: .ghost,
87+
action: {
88+
dismissAction(false)
89+
}
90+
)
91+
}
92+
}
93+
94+
}
95+
96+
}

0 commit comments

Comments
 (0)