diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj
index 82447c8abe..439d8e79ff 100644
--- a/DuckDuckGo.xcodeproj/project.pbxproj
+++ b/DuckDuckGo.xcodeproj/project.pbxproj
@@ -14498,7 +14498,7 @@
repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit";
requirement = {
kind = exactVersion;
- version = 200.2.1;
+ version = 200.3.0;
};
};
9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 95b2c0b4f6..4619f0d1e4 100644
--- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/BrowserServicesKit",
"state" : {
- "revision" : "fdf6f75d570a5ef6058efa881e11f9467627fbf4",
- "version" : "200.2.1"
+ "revision" : "44d747d56bc73cb74de0e9d7127314ea30eca371",
+ "version" : "200.3.0"
}
},
{
@@ -41,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/content-scope-scripts",
"state" : {
- "revision" : "1ed569676555d493c9c5575eaed22aa02569aac9",
- "version" : "6.19.0"
+ "revision" : "b74549bd869fdecc16fad851f2f608b1724764df",
+ "version" : "6.25.0"
}
},
{
@@ -75,7 +75,7 @@
{
"identity" : "lottie-spm",
"kind" : "remoteSourceControl",
- "location" : "https://github.com/airbnb/lottie-spm.git",
+ "location" : "https://github.com/airbnb/lottie-spm",
"state" : {
"revision" : "1d29eccc24cc8b75bff9f6804155112c0ffc9605",
"version" : "4.4.3"
diff --git a/DuckDuckGo/Assets.xcassets/Images/Success-96.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Success-96.imageset/Contents.json
new file mode 100644
index 0000000000..e1afd964fa
--- /dev/null
+++ b/DuckDuckGo/Assets.xcassets/Images/Success-96.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "Success-96x96.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DuckDuckGo/Assets.xcassets/Images/Success-96.imageset/Success-96x96.svg b/DuckDuckGo/Assets.xcassets/Images/Success-96.imageset/Success-96x96.svg
new file mode 100644
index 0000000000..9c4fbc90c9
--- /dev/null
+++ b/DuckDuckGo/Assets.xcassets/Images/Success-96.imageset/Success-96x96.svg
@@ -0,0 +1,15 @@
+
diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift
index 4388d6a568..d9ec142e79 100644
--- a/DuckDuckGo/Common/Localizables/UserText.swift
+++ b/DuckDuckGo/Common/Localizables/UserText.swift
@@ -472,10 +472,12 @@ struct UserText {
static let importPasswords = NSLocalizedString("import.browser.data.passwords", value: "Import Passwords…", comment: "Opens Import Browser Data dialog")
static let importDataTitle = NSLocalizedString("import.browser.data", value: "Import to DuckDuckGo", comment: "Import Browser Data dialog title")
+ static let importDataTitleOnboarding = NSLocalizedString("import.browser.data.onboarding", value: "Great, let’s keep this simple!", comment: "Import Browser Data dialog title")
static let importDataShortcutsTitle = NSLocalizedString("import.browser.data.shortcuts", value: "Almost done!", comment: "Import Browser Data dialog title for final stage when choosing shortcuts to enable")
static let importDataShortcutsSubtitle = NSLocalizedString("import.browser.data.shortcuts.subtitle", value: "You can always right-click on the browser toolbar to find more shortcuts like these.", comment: "Subtitle explaining how users can find toolbar shortcuts.")
- static let importDataSourceTitle = NSLocalizedString("import.browser.data.source.title", value: "Import From", comment: "Import Browser Data title for option to choose source browser to import from")
+ static let importDataSourceTitle = NSLocalizedString("import.browser.data.source.title", value: "Where do you want to import from?", comment: "Import Browser Data title for option to choose source browser to import from")
static let importDataSubtitle = NSLocalizedString("import.browser.data.source.subtitle", value: "Access and manage your passwords in DuckDuckGo Settings > Passwords & Autofill.", comment: "Subtitle explaining where users can find imported passwords.")
+ static let importDataSuccessTitle = NSLocalizedString("import.browser.data.success.title", value: "Import complete!", comment: "message about Passwords and or bookmarks Data Import completion")
static let exportLogins = NSLocalizedString("export.logins.data", value: "Export Passwords…", comment: "Opens Export Logins Data dialog")
static let exportBookmarks = NSLocalizedString("export.bookmarks.menu.item", value: "Export Bookmarks…", comment: "Export bookmarks menu item")
diff --git a/DuckDuckGo/DataImport/Model/DataImportViewModel.swift b/DuckDuckGo/DataImport/Model/DataImportViewModel.swift
index c11c0868b0..3660f02777 100644
--- a/DuckDuckGo/DataImport/Model/DataImportViewModel.swift
+++ b/DuckDuckGo/DataImport/Model/DataImportViewModel.swift
@@ -540,6 +540,10 @@ extension DataImportViewModel {
return DataImportViewSummarizedError(errors: errors)
}
+ var hasAnySummaryError: Bool {
+ !summary.allSatisfy { $0.result.isSuccess }
+ }
+
private static func requestPrimaryPasswordCallback(_ source: DataImport.Source) -> String? {
let alert = NSAlert.passwordRequiredAlert(source: source)
let response = alert.runModal()
diff --git a/DuckDuckGo/DataImport/View/DataImportTypePicker.swift b/DuckDuckGo/DataImport/View/DataImportTypePicker.swift
index 7dbb85d3c5..118d2ba3d9 100644
--- a/DuckDuckGo/DataImport/View/DataImportTypePicker.swift
+++ b/DuckDuckGo/DataImport/View/DataImportTypePicker.swift
@@ -28,7 +28,7 @@ struct DataImportTypePicker: View {
var body: some View {
VStack(alignment: .leading) {
- Text("Select Data to Import:",
+ Text("What do you want to import?",
comment: "Data Import section title for checkboxes of data type to import: Passwords or Bookmarks.")
ForEach(DataImport.DataType.allCases, id: \.self) { dataType in
diff --git a/DuckDuckGo/DataImport/View/DataImportView.swift b/DuckDuckGo/DataImport/View/DataImportView.swift
index dee9ecc021..c3757d7a41 100644
--- a/DuckDuckGo/DataImport/View/DataImportView.swift
+++ b/DuckDuckGo/DataImport/View/DataImportView.swift
@@ -24,7 +24,13 @@ struct DataImportView: ModalView {
@Environment(\.dismiss) private var dismiss
- @State var model = DataImportViewModel()
+ @State var model: DataImportViewModel
+ let title: String
+
+ init(model: DataImportViewModel = DataImportViewModel(), title: String = UserText.importDataTitle) {
+ self._model = State(initialValue: model)
+ self.title = title
+ }
struct ProgressState {
let text: String?
@@ -76,14 +82,27 @@ struct DataImportView: ModalView {
}
private func viewHeader() -> some View {
- VStack(alignment: .leading, spacing: 0) {
- if case .shortcuts = model.screen {
+ return VStack(alignment: .leading, spacing: 0) {
+ // If there are no errors show summary success header
+ if case .summary = model.screen, !model.hasAnySummaryError {
+ VStack(alignment: .leading) {
+ Image(.success96)
+ Text(UserText.importDataSuccessTitle)
+ .foregroundColor(.primary)
+ .font(.system(size: 17, weight: .bold))
+ }
+ .padding(.bottom, 16)
+ } else if case .shortcuts = model.screen {
Text(UserText.importDataShortcutsTitle)
.font(.title2.weight(.semibold))
.padding(.bottom, 24)
} else {
- Text(UserText.importDataTitle)
+ // If screen is not the first screen where the user choose the type of import they want to do show the generic title.
+ // Otherwise show the injected title.
+ let title = model.screen == .profileAndDataTypesPicker ? self.title : UserText.importDataTitle
+
+ Text(title)
.font(.title2.weight(.semibold))
.padding(.bottom, 24)
diff --git a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift
index 11fa3c64d5..a14221a965 100644
--- a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift
+++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift
@@ -32,7 +32,7 @@ public enum FeatureFlag: String {
// https://app.asana.com/0/1206488453854252/1207136666798700/f
case freemiumPIR
- case highlightsOnboarding
+ case contextualOnboarding
// https://app.asana.com/0/1201462886803403/1208030658792310/f
case unknownUsernameCategorization
@@ -60,8 +60,8 @@ extension FeatureFlag: FeatureFlagSourceProviding {
return .remoteReleasable(.subfeature(PhishingDetectionSubfeature.allowErrorPage))
case .phishingDetectionPreferences:
return .remoteReleasable(.subfeature(PhishingDetectionSubfeature.allowPreferencesToggle))
- case .highlightsOnboarding:
- return .internalOnly
+ case .contextualOnboarding:
+ return .remoteReleasable(.feature(.contextualOnboarding))
case .credentialsImportPromotionForExistingUsers:
return .remoteReleasable(.subfeature(AutofillSubfeature.credentialsImportPromotionForExistingUsers))
case .networkProtectionUserTips:
diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings
index 9ac1b9af4d..918c1bea94 100644
--- a/DuckDuckGo/Localizable.xcstrings
+++ b/DuckDuckGo/Localizable.xcstrings
@@ -28231,6 +28231,66 @@
}
}
},
+ "import.browser.data.onboarding" : {
+ "comment" : "Import Browser Data dialog title",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Super, halten wir es unkompliziert!"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Great, let’s keep this simple!"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "¡Estupendo, hagámoslo de manera sencilla!"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Super, restons simples !"
+ }
+ },
+ "it" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Ottimo, semplifichiamo le cose!"
+ }
+ },
+ "nl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Geweldig, laten we dit eenvoudig houden!"
+ }
+ },
+ "pl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Świetnie, to proste!"
+ }
+ },
+ "pt" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Ótimo, vamos manter isto simples!"
+ }
+ },
+ "ru" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Отлично! Зачем усложнять?"
+ }
+ }
+ }
+ },
"import.browser.data.passwords" : {
"comment" : "Opens Import Browser Data dialog",
"extractionState" : "extracted_with_value",
@@ -28478,55 +28538,115 @@
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Importieren aus"
+ "value" : "Woher möchtest du importieren?"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Where do you want to import from?"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "¿De dónde quieres importar?"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "D'où voulez-vous importer ?"
+ }
+ },
+ "it" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Da dove vuoi importare?"
+ }
+ },
+ "nl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Vanwaar wil je importeren?"
+ }
+ },
+ "pl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Skąd chcesz importować?"
+ }
+ },
+ "pt" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "De onde queres importar?"
+ }
+ },
+ "ru" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Откуда импортировать данные?"
+ }
+ }
+ }
+ },
+ "import.browser.data.success.title" : {
+ "comment" : "message about Passwords and or bookmarks Data Import completion",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Import abgeschlossen!"
}
},
"en" : {
"stringUnit" : {
"state" : "new",
- "value" : "Import From"
+ "value" : "Import complete!"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Importar desde"
+ "value" : "¡Importación completada!"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Importer depuis"
+ "value" : "Importation terminée !"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Importa da"
+ "value" : "Importazione completata!"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Importeren vanuit"
+ "value" : "Importeren voltooid!"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Importuj z"
+ "value" : "Import zakończony!"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Importar de"
+ "value" : "Importação concluída!"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Импорт из"
+ "value" : "Импортирование завершено."
}
}
}
@@ -55563,6 +55683,7 @@
},
"Select Data to Import:" : {
"comment" : "Data Import section title for checkboxes of data type to import: Passwords or Bookmarks.",
+ "extractionState" : "stale",
"localizations" : {
"de" : {
"stringUnit" : {
@@ -62370,6 +62491,59 @@
}
}
},
+ "What do you want to import?" : {
+ "comment" : "Data Import section title for checkboxes of data type to import: Passwords or Bookmarks.",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Was möchtest du importieren?"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "¿Qué quieres importar?"
+ }
+ },
+ "fr" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Que voulez-vous importer ?"
+ }
+ },
+ "it" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cosa vuoi importare?"
+ }
+ },
+ "nl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Wat wil je importeren?"
+ }
+ },
+ "pl" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Co chcesz importować?"
+ }
+ },
+ "pt" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "O que queres importar?"
+ }
+ },
+ "ru" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Что будем импортировать?"
+ }
+ }
+ }
+ },
"whats.new.menu.item" : {
"comment" : "Title of the dialog menu item that opens the 'What's New' page",
"extractionState" : "extracted_with_value",
diff --git a/DuckDuckGo/Onboarding/ContextualOnboarding/ContextualOnboardingStateMachine.swift b/DuckDuckGo/Onboarding/ContextualOnboarding/ContextualOnboardingStateMachine.swift
index e2f79ae7d4..71bd9dc48a 100644
--- a/DuckDuckGo/Onboarding/ContextualOnboarding/ContextualOnboardingStateMachine.swift
+++ b/DuckDuckGo/Onboarding/ContextualOnboarding/ContextualOnboardingStateMachine.swift
@@ -28,6 +28,7 @@ protocol ContextualOnboardingStateUpdater: AnyObject {
func updateStateFor(tab: Tab)
func gotItPressed()
func fireButtonUsed()
+ func turnOffFeature()
}
protocol FireButtonInfoStateProviding {
@@ -381,4 +382,8 @@ final class ContextualOnboardingStateMachine: ContextualOnboardingDialogTypeProv
break
}
}
+
+ func turnOffFeature() {
+ state = .onboardingCompleted
+ }
}
diff --git a/DuckDuckGo/Onboarding/ContextualOnboarding/ViewHighlighter/ContextualOnboardingViewHighlighter.swift b/DuckDuckGo/Onboarding/ContextualOnboarding/ViewHighlighter/ContextualOnboardingViewHighlighter.swift
new file mode 100644
index 0000000000..1ce885facc
--- /dev/null
+++ b/DuckDuckGo/Onboarding/ContextualOnboarding/ViewHighlighter/ContextualOnboardingViewHighlighter.swift
@@ -0,0 +1,81 @@
+//
+// ContextualOnboardingViewHighlighter.swift
+//
+// Copyright © 2024 DuckDuckGo. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import AppKit
+import Lottie
+
+enum ContextualOnboardingViewHighlighter {
+
+ private static let identifier = "lottie_pulse_animation_view"
+
+ static func highlight(view: NSView, inParent parent: NSView) {
+ // Avoid duplicate animations
+ guard !isViewHighlighted(view) else { return }
+
+ let animationView = LottieAnimationView.makePulseAnimationView()
+ animationView.identifier = NSUserInterfaceItemIdentifier(identifier)
+ let multiplier = 2.5
+ parent.addSubview(animationView)
+
+ NSLayoutConstraint.activate([
+ animationView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ animationView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
+ animationView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: multiplier),
+ animationView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: multiplier),
+ ])
+ }
+
+ static func stopHighlighting(view: NSView) {
+ guard let view = findViewWithIdentifier(in: view, identifier: NSUserInterfaceItemIdentifier(identifier)) else { return }
+ view.removeFromSuperview()
+ }
+
+ static func isViewHighlighted(_ view: NSView) -> Bool {
+ findViewWithIdentifier(in: view, identifier: NSUserInterfaceItemIdentifier(identifier)) != nil
+ }
+
+ private static func findViewWithIdentifier(in view: NSView, identifier: NSUserInterfaceItemIdentifier) -> NSView? {
+ // Check the current view's superview
+ // If no matching view is found, return nil
+ guard let superview = view.superview else { return nil }
+
+ // Check all subviews of the superview
+ for subview in superview.subviews where subview.identifier == identifier {
+ return subview
+ }
+
+ // Recursively check the superview's superview
+ return findViewWithIdentifier(in: superview, identifier: identifier)
+
+ }
+
+}
+
+extension LottieAnimationView {
+
+ static func makePulseAnimationView() -> LottieAnimationView {
+ let animation = LottieAnimation.named("view_highlight")
+ let animationView = LottieAnimationView(animation: animation)
+ animationView.contentMode = .scaleToFill
+ animationView.loopMode = .loop
+ animationView.play()
+ animationView.translatesAutoresizingMaskIntoConstraints = false
+ return animationView
+ }
+
+}
diff --git a/DuckDuckGo/Onboarding/OnboardingActionsManager.swift b/DuckDuckGo/Onboarding/OnboardingActionsManager.swift
index 22321ea4cc..1ba94165b9 100644
--- a/DuckDuckGo/Onboarding/OnboardingActionsManager.swift
+++ b/DuckDuckGo/Onboarding/OnboardingActionsManager.swift
@@ -23,12 +23,11 @@ import Common
import os.log
enum OnboardingSteps: String, CaseIterable {
- case summary
case welcome
case getStarted
- case privateByDefault
- case cleanerBrowsing
+ case makeDefaultSingle
case systemSettings
+ case duckPlayerSingle
case customize
}
@@ -91,20 +90,23 @@ final class OnboardingActionsManager: OnboardingActionsManaging {
let configuration: OnboardingConfiguration = {
var systemSettings: SystemSettings
+ var order = "v3"
+ let platform = OnboardingPlatform(name: "macos")
#if APPSTORE
- systemSettings = SystemSettings(rows: ["import", "default-browser"])
+ systemSettings = SystemSettings(rows: ["import"])
#else
- systemSettings = SystemSettings(rows: ["dock", "import", "default-browser"])
+ systemSettings = SystemSettings(rows: ["dock", "import"])
#endif
let stepDefinitions = StepDefinitions(systemSettings: systemSettings)
let preferredLocale = Bundle.main.preferredLocalizations.first ?? "en"
var env: String
-#if DEBUG
+#if DEBUG || REVIEW
env = "development"
#else
env = "production"
#endif
- return OnboardingConfiguration(stepDefinitions: stepDefinitions, env: env, locale: preferredLocale)
+
+ return OnboardingConfiguration(stepDefinitions: stepDefinitions, exclude: [], order: order, env: env, locale: preferredLocale, platform: platform)
}()
init(navigationDelegate: OnboardingNavigating, dockCustomization: DockCustomization, defaultBrowserProvider: DefaultBrowserProvider, appearancePreferences: AppearancePreferences, startupPreferences: StartupPreferences) {
@@ -121,6 +123,7 @@ final class OnboardingActionsManager: OnboardingActionsManaging {
@MainActor
func goToAddressBar() {
+ PixelKit.fire(GeneralPixel.onboardingStepCompleteCustomize, frequency: .legacyDaily)
onboardingHasFinished()
let tab = Tab(content: .url(URL.duckDuckGo, source: .ui))
navigation.replaceTabWith(tab)
@@ -178,18 +181,16 @@ final class OnboardingActionsManager: OnboardingActionsManaging {
func stepCompleted(step: OnboardingSteps) {
switch step {
- case .summary:
- break
case .welcome:
PixelKit.fire(GeneralPixel.onboardingStepCompleteWelcome, frequency: .legacyDaily)
case .getStarted:
PixelKit.fire(GeneralPixel.onboardingStepCompleteGetStarted, frequency: .legacyDaily)
- case .privateByDefault:
+ case .makeDefaultSingle:
PixelKit.fire(GeneralPixel.onboardingStepCompletePrivateByDefault, frequency: .legacyDaily)
- case .cleanerBrowsing:
- PixelKit.fire(GeneralPixel.onboardingStepCompleteCleanerBrowsing, frequency: .legacyDaily)
case .systemSettings:
PixelKit.fire(GeneralPixel.onboardingStepCompleteSystemSettings, frequency: .legacyDaily)
+ case .duckPlayerSingle:
+ PixelKit.fire(GeneralPixel.onboardingStepCompleteCleanerBrowsing, frequency: .legacyDaily)
case .customize:
PixelKit.fire(GeneralPixel.onboardingStepCompleteCustomize, frequency: .legacyDaily)
}
diff --git a/DuckDuckGo/Onboarding/OnboardingConfiguration.swift b/DuckDuckGo/Onboarding/OnboardingConfiguration.swift
index 92019506fc..9477fe183c 100644
--- a/DuckDuckGo/Onboarding/OnboardingConfiguration.swift
+++ b/DuckDuckGo/Onboarding/OnboardingConfiguration.swift
@@ -21,8 +21,11 @@ import Foundation
/// Configuration needed to set up the FE onboarding
struct OnboardingConfiguration: Codable, Equatable {
var stepDefinitions: StepDefinitions
+ var exclude: [String]
+ var order: String
var env: String
var locale: String
+ var platform: OnboardingPlatform
}
/// Defines the onboarding steps desired
@@ -33,3 +36,7 @@ struct StepDefinitions: Codable, Equatable {
struct SystemSettings: Codable, Equatable {
var rows: [String]
}
+
+struct OnboardingPlatform: Codable, Equatable {
+ var name: String
+}
diff --git a/DuckDuckGo/Statistics/Experiment/PixelExperiment.swift b/DuckDuckGo/Statistics/Experiment/PixelExperiment.swift
index 04465aa90f..b23bc40d9b 100644
--- a/DuckDuckGo/Statistics/Experiment/PixelExperiment.swift
+++ b/DuckDuckGo/Statistics/Experiment/PixelExperiment.swift
@@ -60,8 +60,8 @@ enum PixelExperiment: String, CaseIterable {
// These are the variants. Rename or add/remove them as needed. If you change the string value
// remember to keep it clear for privacy triage.
- case control = "oa"
- case newOnboarding = "ob"
+ case control = "oc"
+ case newOnboarding = "od"
}
// These functions contain the business logic for determining if the pixel should be fired or not.
diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift
index 4f3867b84e..af5691c907 100644
--- a/DuckDuckGo/Tab/Model/Tab.swift
+++ b/DuckDuckGo/Tab/Model/Tab.swift
@@ -776,6 +776,7 @@ protocol NewWindowPolicyDecisionMaker {
}
}
+ @MainActor
func startOnboarding() {
userInteractionDialog = nil
@@ -787,6 +788,7 @@ protocol NewWindowPolicyDecisionMaker {
#endif
if PixelExperiment.cohort == .newOnboarding {
+ Application.appDelegate.onboardingStateMachine.state = .notStarted
setContent(.onboarding)
} else {
setContent(.onboardingDeprecated)
diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift
index 7b4084ecaa..a400a683ce 100644
--- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift
+++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift
@@ -407,6 +407,10 @@ final class BrowserTabViewController: NSViewController {
}
private func updateStateAndPresentContextualOnboarding() {
+ guard featureFlagger.isFeatureOn(.contextualOnboarding) else {
+ onboardingDialogTypeProvider.turnOffFeature()
+ return
+ }
guard let tab = tabViewModel?.tab else { return }
onboardingDialogTypeProvider.updateStateFor(tab: tab)
presentContextualOnboarding()
@@ -425,7 +429,10 @@ final class BrowserTabViewController: NSViewController {
// Remove any existing higlights animation
delegate?.dismissViewHighlight()
- guard featureFlagger.isFeatureOn(.highlightsOnboarding) else { return }
+ guard featureFlagger.isFeatureOn(.contextualOnboarding) else {
+ onboardingDialogTypeProvider.turnOffFeature()
+ return
+ }
guard let tab = tabViewModel?.tab else { return }
guard let dialogType = onboardingDialogTypeProvider.dialogTypeForTab(tab, privacyInfo: tab.privacyInfo) else {
diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift
index 3bfaa6dfcd..90e8f59ec7 100644
--- a/DuckDuckGo/Windows/View/WindowControllersManager.swift
+++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift
@@ -354,7 +354,7 @@ extension WindowControllersManager: OnboardingNavigating {
@MainActor
func showImportDataView() {
- DataImportView().show()
+ DataImportView(title: UserText.importDataTitleOnboarding).show()
}
@MainActor
diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift
index bbd5ac6cbc..c640e98947 100644
--- a/LocalPackages/DataBrokerProtection/Package.swift
+++ b/LocalPackages/DataBrokerProtection/Package.swift
@@ -29,7 +29,7 @@ let package = Package(
targets: ["DataBrokerProtection"])
],
dependencies: [
- .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "200.2.1"),
+ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "200.3.0"),
.package(path: "../SwiftUIExtensions"),
.package(path: "../XPCHelper"),
],
diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift
index 033d1ff14a..a187eb6657 100644
--- a/LocalPackages/NetworkProtectionMac/Package.swift
+++ b/LocalPackages/NetworkProtectionMac/Package.swift
@@ -32,7 +32,7 @@ let package = Package(
.library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]),
],
dependencies: [
- .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "200.2.1"),
+ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "200.3.0"),
.package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"),
.package(path: "../AppLauncher"),
.package(path: "../UDSHelper"),
diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift
index 72475dd736..6d8d57bc35 100644
--- a/LocalPackages/SubscriptionUI/Package.swift
+++ b/LocalPackages/SubscriptionUI/Package.swift
@@ -12,7 +12,7 @@ let package = Package(
targets: ["SubscriptionUI"]),
],
dependencies: [
- .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "200.2.1"),
+ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "200.3.0"),
.package(path: "../SwiftUIExtensions")
],
targets: [
diff --git a/UnitTests/DataImport/DataImportViewModelTests.swift b/UnitTests/DataImport/DataImportViewModelTests.swift
index 37ff6db51a..0e245b9015 100644
--- a/UnitTests/DataImport/DataImportViewModelTests.swift
+++ b/UnitTests/DataImport/DataImportViewModelTests.swift
@@ -103,6 +103,46 @@ final class DataImportViewModelTests: XCTestCase {
await fulfillment(of: [e2, eDismissed], timeout: 0)
}
+ func testWhenImportSummaryCompletesWithErrorsThenHasSummaryErrorsShouldReturnTrue() async throws {
+ // GIVEN
+ for source in Source.allCases where source.initialScreen == .profileAndDataTypesPicker {
+ // GIVEN
+ let browser = try XCTUnwrap(ThirdPartyBrowser.browser(for: source))
+ for dataType in DataType.allCases {
+ setupModel(with: source, profiles: [BrowserProfile.test])
+ try await initiateImport(of: source.supportedDataTypes, from: .test(for: browser), resultingWith: [
+ dataType: .failure(Failure(.bookmarks, DataImport.ErrorType.dataCorrupted))
+ ])
+
+ // WHEN
+ let result = model.hasAnySummaryError
+
+ // THEN
+ XCTAssertTrue(result)
+ }
+ }
+ }
+
+ func testWhenImportSummaryCompletesWithoutErrorsThenHasSummaryErrorsShouldReturnFalse() async throws {
+ // GIVEN
+ for source in Source.allCases where source.initialScreen == .profileAndDataTypesPicker {
+ // GIVEN
+ let browser = try XCTUnwrap(ThirdPartyBrowser.browser(for: source))
+ for dataType in DataType.allCases {
+ setupModel(with: source, profiles: [BrowserProfile.test, BrowserProfile.default, BrowserProfile.test2])
+ try await initiateImport(of: source.supportedDataTypes, from: .test(for: browser), resultingWith: [
+ dataType: .success(.init(successful: 10, duplicate: 0, failed: 1))
+ ])
+
+ // WHEN
+ let result = model.hasAnySummaryError
+
+ // THEN
+ XCTAssertFalse(result)
+ }
+ }
+ }
+
// MARK: - Browser profiles
func testWhenNoProfilesAreLoaded_selectedProfileIsNil() {
diff --git a/UnitTests/Fire/Model/FirePopoverViewModelTests.swift b/UnitTests/Fire/Model/FirePopoverViewModelTests.swift
index 281be427b8..79a155ba0d 100644
--- a/UnitTests/Fire/Model/FirePopoverViewModelTests.swift
+++ b/UnitTests/Fire/Model/FirePopoverViewModelTests.swift
@@ -64,6 +64,7 @@ final class FirePopoverViewModelTests: XCTestCase {
}
class CapturingContextualOnboardingStateUpdater: ContextualOnboardingStateUpdater {
+
var state: ContextualOnboardingState = .onboardingCompleted
var updatedForTab: Tab?
@@ -82,4 +83,6 @@ class CapturingContextualOnboardingStateUpdater: ContextualOnboardingStateUpdate
fireButtonUsedCalled = true
}
+ func turnOffFeature() {}
+
}
diff --git a/UnitTests/Onboarding/ContextualOnboarding/BrowserTabViewControllerOnboardingTests.swift b/UnitTests/Onboarding/ContextualOnboarding/BrowserTabViewControllerOnboardingTests.swift
index c066e635b8..e94c5879dd 100644
--- a/UnitTests/Onboarding/ContextualOnboarding/BrowserTabViewControllerOnboardingTests.swift
+++ b/UnitTests/Onboarding/ContextualOnboarding/BrowserTabViewControllerOnboardingTests.swift
@@ -29,20 +29,23 @@ final class BrowserTabViewControllerOnboardingTests: XCTestCase {
var viewController: BrowserTabViewController!
var dialogProvider: MockDialogsProvider!
var factory: CapturingDialogFactory!
+ var featureFlagger: MockFeatureFlagger!
var tab: Tab!
var cancellables: Set = []
var expectation: XCTestExpectation!
+ var dialogTypeForTabExpectation: XCTestExpectation!
@MainActor override func setUpWithError() throws {
try super.setUpWithError()
let tabCollectionViewModel = TabCollectionViewModel()
+ featureFlagger = MockFeatureFlagger()
dialogProvider = MockDialogsProvider()
expectation = .init()
factory = CapturingDialogFactory(expectation: expectation)
tab = Tab()
tab.setContent(.url(URL.duckDuckGo, credential: nil, source: .appOpenUrl))
let tabViewModel = TabViewModel(tab: tab)
- viewController = BrowserTabViewController(tabCollectionViewModel: tabCollectionViewModel, onboardingDialogTypeProvider: dialogProvider, onboardingDialogFactory: factory, featureFlagger: MockFeatureFlagger())
+ viewController = BrowserTabViewController(tabCollectionViewModel: tabCollectionViewModel, onboardingDialogTypeProvider: dialogProvider, onboardingDialogFactory: factory, featureFlagger: featureFlagger)
viewController.tabViewModel = tabViewModel
let window = NSWindow()
window.contentViewController = viewController
@@ -56,9 +59,20 @@ final class BrowserTabViewControllerOnboardingTests: XCTestCase {
viewController = nil
cancellables = []
expectation = nil
+ featureFlagger = nil
try super.tearDownWithError()
}
+ func testWhenNavigationCompletedAndFeatureIsOffThenTurnOffFeature() throws {
+ featureFlagger.isFeatureOn = false
+ let expectation = self.expectation(description: "Wait for turnOffFeatureCalled to be called")
+ dialogProvider.turnOffFeatureCalledExpectation = expectation
+
+ tab.navigateTo(url: URL(string: "some.url")!)
+
+ wait(for: [expectation], timeout: 3.0)
+ }
+
func testWhenNavigationCompletedAndNoDialogTypeThenOnlyWebViewVisible() throws {
let expectation = self.expectation(description: "Wait for webViewDidFinishNavigationPublisher to emit")
tab.navigateTo(url: URL(string: "some.url")!)
@@ -251,7 +265,9 @@ final class BrowserTabViewControllerOnboardingTests: XCTestCase {
}
class MockDialogsProvider: ContextualOnboardingDialogTypeProviding, ContextualOnboardingStateUpdater {
+
var state: ContextualOnboardingState = .onboardingCompleted
+ var turnOffFeatureCalledExpectation: XCTestExpectation?
func updateStateFor(tab: DuckDuckGo_Privacy_Browser.Tab) {}
@@ -267,6 +283,10 @@ class MockDialogsProvider: ContextualOnboardingDialogTypeProviding, ContextualOn
func gotItPressed() {}
func fireButtonUsed() {}
+
+ func turnOffFeature() {
+ turnOffFeatureCalledExpectation?.fulfill()
+ }
}
class CapturingDialogFactory: ContextualDaxDialogsFactory {
diff --git a/UnitTests/Onboarding/ContextualOnboarding/ContextualOnboardingStateMachineTests.swift b/UnitTests/Onboarding/ContextualOnboarding/ContextualOnboardingStateMachineTests.swift
index 178782543a..82cce0838c 100644
--- a/UnitTests/Onboarding/ContextualOnboarding/ContextualOnboardingStateMachineTests.swift
+++ b/UnitTests/Onboarding/ContextualOnboarding/ContextualOnboardingStateMachineTests.swift
@@ -673,6 +673,17 @@ class ContextualOnboardingStateMachineTests: XCTestCase {
// Then
XCTAssertEqual(stateMachine.state, .showBlockedTrackers)
}
+
+ func test_OnTurnOffFeature_StateBecomesOnboardingCompleted() {
+ // Given
+ stateMachine.state = .showTryASearch
+
+ // When
+ stateMachine.turnOffFeature()
+
+ // Then
+ XCTAssertEqual(stateMachine.state, .onboardingCompleted)
+ }
}
class MockTrackerMessageProvider: TrackerMessageProviding {
diff --git a/UnitTests/Onboarding/ContextualOnboarding/ContextualOnboardingViewHighlighterTests.swift b/UnitTests/Onboarding/ContextualOnboarding/ContextualOnboardingViewHighlighterTests.swift
new file mode 100644
index 0000000000..79778aee4b
--- /dev/null
+++ b/UnitTests/Onboarding/ContextualOnboarding/ContextualOnboardingViewHighlighterTests.swift
@@ -0,0 +1,105 @@
+//
+// ContextualOnboardingViewHighlighterTests.swift
+//
+// Copyright © 2024 DuckDuckGo. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import XCTest
+@testable import DuckDuckGo_Privacy_Browser
+
+final class ContextualOnboardingViewHighlighterTests: XCTestCase {
+
+ func testWhenHighlightViewIsCalledThenViewShouldContainAnimationView() {
+ // GIVEN
+ let (parent, child) = makeDummyViews()
+
+ // WHEN
+ ContextualOnboardingViewHighlighter.highlight(view: child, inParent: parent)
+
+ // THEN
+ XCTAssertNotNil(findFirstAnimationView(in: parent))
+ }
+
+ func testWhenIsViewIsHighligthedIsCalledAndViewIsHighlightedThenReturnTrue() {
+ // GIVEN
+ let (parent, child) = makeDummyViews()
+ XCTAssertFalse(ContextualOnboardingViewHighlighter.isViewHighlighted(child))
+ ContextualOnboardingViewHighlighter.highlight(view: child, inParent: parent)
+
+ // WHEN
+ let result = ContextualOnboardingViewHighlighter.isViewHighlighted(child)
+
+ // THEN
+ XCTAssertTrue(result)
+ }
+
+ func testWhenIsViewIsNotHighligthedIsCalledAndViewIsHighlightedThenReturnFalse() {
+ // GIVEN
+ let (_, child) = makeDummyViews()
+ XCTAssertFalse(ContextualOnboardingViewHighlighter.isViewHighlighted(child))
+
+ // WHEN
+ let result = ContextualOnboardingViewHighlighter.isViewHighlighted(child)
+
+ // THEN
+ XCTAssertFalse(result)
+ }
+
+ func testWhenStopHighlightingViewIsCalledThenAnimationViewIsRemoved() {
+ // GIVEN
+ let (parent, child) = makeDummyViews()
+ ContextualOnboardingViewHighlighter.highlight(view: child, inParent: parent)
+ XCTAssertNotNil(findAllAnimationView(in: parent))
+
+ // WHEN
+ ContextualOnboardingViewHighlighter.stopHighlighting(view: child)
+
+ // THEN
+ XCTAssertNil(findFirstAnimationView(in: parent))
+ }
+
+ func testWhenViewIsHighlightedAndHighlightViewIsCalledThenNothingHappens() {
+ // GIVEN
+ let (parent, child) = makeDummyViews()
+ ContextualOnboardingViewHighlighter.highlight(view: child, inParent: parent)
+ XCTAssertEqual(findAllAnimationView(in: parent).count, 1)
+
+ // WHEN
+ ContextualOnboardingViewHighlighter.highlight(view: child, inParent: parent)
+
+ // THEN
+ XCTAssertEqual(findAllAnimationView(in: parent).count, 1)
+ }
+
+}
+
+private extension ContextualOnboardingViewHighlighterTests {
+
+ func makeDummyViews() -> (parent: NSView, child: NSView) {
+ let parent = NSView()
+ let child = NSView()
+ parent.addSubview(child)
+ return (parent, child)
+ }
+
+ func findAllAnimationView(in view: NSView) -> [NSView] {
+ view.subviews.filter { $0.identifier == NSUserInterfaceItemIdentifier("lottie_pulse_animation_view") }
+ }
+
+ func findFirstAnimationView(in view: NSView) -> NSView? {
+ findAllAnimationView(in: view).first
+ }
+
+}
diff --git a/UnitTests/Onboarding/ContextualOnboarding/OnboardingPixelReporterTests.swift b/UnitTests/Onboarding/ContextualOnboarding/OnboardingPixelReporterTests.swift
index 86465a723c..9df50c7f24 100644
--- a/UnitTests/Onboarding/ContextualOnboarding/OnboardingPixelReporterTests.swift
+++ b/UnitTests/Onboarding/ContextualOnboarding/OnboardingPixelReporterTests.swift
@@ -149,6 +149,7 @@ final class OnboardingPixelReporterTests: XCTestCase {
}
class MockContextualOnboardingState: ContextualOnboardingStateUpdater {
+
var state: ContextualOnboardingState = .onboardingCompleted
func updateStateFor(tab: Tab) {
@@ -160,4 +161,6 @@ class MockContextualOnboardingState: ContextualOnboardingStateUpdater {
func fireButtonUsed() {
}
+ func turnOffFeature() {}
+
}
diff --git a/UnitTests/Onboarding/Mocks/CapturingOnboardingActionsManager.swift b/UnitTests/Onboarding/Mocks/CapturingOnboardingActionsManager.swift
index 78eaf12eba..afe4a93bb6 100644
--- a/UnitTests/Onboarding/Mocks/CapturingOnboardingActionsManager.swift
+++ b/UnitTests/Onboarding/Mocks/CapturingOnboardingActionsManager.swift
@@ -21,7 +21,14 @@ import Foundation
class CapturingOnboardingActionsManager: OnboardingActionsManaging {
- var configuration: OnboardingConfiguration = OnboardingConfiguration(stepDefinitions: StepDefinitions(systemSettings: SystemSettings(rows: [])), env: "environment", locale: "en")
+ var configuration: OnboardingConfiguration = OnboardingConfiguration(
+ stepDefinitions: StepDefinitions(systemSettings: SystemSettings(rows: [])),
+ exclude: [],
+ order: "",
+ env: "environment",
+ locale: "en",
+ platform: .init(name: "")
+ )
var goToAddressBarCalled = false
var goToSettingsCalled = false
diff --git a/UnitTests/Onboarding/OnboardingManagerTests.swift b/UnitTests/Onboarding/OnboardingManagerTests.swift
index 785588ce7d..0d5bdad199 100644
--- a/UnitTests/Onboarding/OnboardingManagerTests.swift
+++ b/UnitTests/Onboarding/OnboardingManagerTests.swift
@@ -57,12 +57,19 @@ class OnboardingManagerTests: XCTestCase {
// Given
var systemSettings: SystemSettings
#if APPSTORE
- systemSettings = SystemSettings(rows: ["import", "default-browser"])
+ systemSettings = SystemSettings(rows: ["import"])
#else
- systemSettings = SystemSettings(rows: ["dock", "import", "default-browser"])
+ systemSettings = SystemSettings(rows: ["dock", "import"])
#endif
let stepDefinitions = StepDefinitions(systemSettings: systemSettings)
- let expectedConfig = OnboardingConfiguration(stepDefinitions: stepDefinitions, env: "development", locale: "en")
+ let expectedConfig = OnboardingConfiguration(
+ stepDefinitions: stepDefinitions,
+ exclude: [],
+ order: "v3",
+ env: "development",
+ locale: "en",
+ platform: .init(name: "macos")
+ )
// Then
XCTAssertEqual(manager.configuration, expectedConfig)
diff --git a/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift b/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift
index e4fad9262e..ffa3b7d9c6 100644
--- a/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift
+++ b/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift
@@ -480,7 +480,8 @@ class ChallangeSender: URLAuthenticationChallengeSender {
}
class MockFeatureFlagger: FeatureFlagger {
+ var isFeatureOn = true
func isFeatureOn(forProvider: F) -> Bool where F: BrowserServicesKit.FeatureFlagSourceProviding {
- return true
+ return isFeatureOn
}
}