diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj
index a5a233c122..986e587e33 100644
--- a/DuckDuckGo.xcodeproj/project.pbxproj
+++ b/DuckDuckGo.xcodeproj/project.pbxproj
@@ -12778,7 +12778,7 @@
repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit";
requirement = {
kind = exactVersion;
- version = 144.0.1;
+ version = "144.0.1-1";
};
};
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 dd9eae0f55..ec24112bc8 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" : "f34b0a63938df11ef471aa3301dcc0de09b0d31b",
- "version" : "144.0.1"
+ "revision" : "6ceabf1d257ff1d1164afb5b9139f9f20baf0c6e",
+ "version" : "144.0.1-1"
}
},
{
@@ -50,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/duckduckgo-autofill.git",
"state" : {
- "revision" : "6053999d6af384a716ab0ce7205dbab5d70ed1b3",
- "version" : "11.0.1"
+ "revision" : "10aeff1ec7f533d1705233a9b14f9393a699b1c0",
+ "version" : "11.0.2"
}
},
{
diff --git a/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Contents.json
new file mode 100644
index 0000000000..1c9ce942d3
--- /dev/null
+++ b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "Passwords-DDG-128.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Passwords-DDG-128.svg b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Passwords-DDG-128.svg
new file mode 100644
index 0000000000..fefd0c6886
--- /dev/null
+++ b/DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Passwords-DDG-128.svg
@@ -0,0 +1,31 @@
+
diff --git a/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift b/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift
index 15ca4a8c76..a431276800 100644
--- a/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift
+++ b/DuckDuckGo/Common/Surveys/SurveyURLBuilder.swift
@@ -18,6 +18,7 @@
import Foundation
import Common
+import BrowserServicesKit
final class SurveyURLBuilder {
@@ -95,6 +96,23 @@ final class SurveyURLBuilder {
return components.url
}
+ func buildSurveyURLWithPasswordsCountSurveyParameter(from originalURLString: String) -> URL? {
+ let surveyURLWithParameters = buildSurveyURL(from: originalURLString)
+
+ guard let surveyURLWithParametersString = surveyURLWithParameters?.absoluteString,
+ var components = URLComponents(string: surveyURLWithParametersString),
+ let bucket = passwordsCountBucket() else {
+ return surveyURLWithParameters
+ }
+
+ var queryItems = components.queryItems ?? []
+ queryItems.append(URLQueryItem(name: "saved_passwords", value: bucket))
+
+ components.queryItems = queryItems
+
+ return components.url
+ }
+
private func queryItem(parameter: SurveyURLParameters, value: String) -> URLQueryItem {
let urlAllowed: CharacterSet = .alphanumerics.union(.init(charactersIn: "-._~"))
let sanitizedValue = value.addingPercentEncoding(withAllowedCharacters: urlAllowed)
@@ -105,4 +123,13 @@ final class SurveyURLBuilder {
return URLQueryItem(name: parameter.rawValue, value: String(describing: value))
}
+ private func passwordsCountBucket() -> String? {
+ guard let secureVault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared),
+ let bucket = try? secureVault.accountsCountBucket() else {
+ return nil
+ }
+
+ return bucket
+ }
+
}
diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
index 5891b891a5..5876bef57d 100644
--- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
+++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
@@ -74,6 +74,7 @@ public struct UserDefaultsWrapper {
case askToSavePaymentMethods = "preferences.ask-to-save.payment-methods"
case autolockLocksFormFilling = "preferences.lock-autofill-form-fill"
case autofillDebugScriptEnabled = "preferences.enable-autofill-debug-script"
+ case autofillSurveyEnabled = "preferences.enable-autofill-survey"
case saveAsPreferredFileType = "saveAs.selected.filetype"
diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift
index 66e026dc1d..1b24150862 100644
--- a/DuckDuckGo/Menus/MainMenu.swift
+++ b/DuckDuckGo/Menus/MainMenu.swift
@@ -584,6 +584,7 @@ import SubscriptionUI
}
NSMenuItem(title: "Reset Email Protection InContext Signup Prompt", action: #selector(MainViewController.resetEmailProtectionInContextPrompt))
NSMenuItem(title: "Reset Pixels Storage", action: #selector(MainViewController.resetDailyPixels))
+ NSMenuItem(title: "Reset Passwords Survey", action: #selector(enablePasswordsSurveyAction), target: self)
}.withAccessibilityIdentifier("MainMenu.resetData")
NSMenuItem(title: "UI Triggers") {
NSMenuItem(title: "Show Save Credentials Popover", action: #selector(MainViewController.showSaveCredentialsPopover))
@@ -729,6 +730,10 @@ import SubscriptionUI
updateAutofillDebugScriptMenuItem()
}
+ @objc private func enablePasswordsSurveyAction(_ sender: NSMenuItem) {
+ AutofillPreferences().autofillSurveyEnabled = true
+ }
+
@objc private func debugLoggingMenuItemAction(_ sender: NSMenuItem) {
#if APPSTORE
if !OSLog.isRunningInDebugEnvironment {
diff --git a/DuckDuckGo/Preferences/Model/AutofillPreferences.swift b/DuckDuckGo/Preferences/Model/AutofillPreferences.swift
index 58a55e86f1..78920dd0c0 100644
--- a/DuckDuckGo/Preferences/Model/AutofillPreferences.swift
+++ b/DuckDuckGo/Preferences/Model/AutofillPreferences.swift
@@ -27,6 +27,7 @@ protocol AutofillPreferencesPersistor {
var autolockLocksFormFilling: Bool { get set }
var passwordManager: PasswordManager { get set }
var debugScriptEnabled: Bool { get set }
+ var autofillSurveyEnabled: Bool { get set }
}
enum PasswordManager: String, CaseIterable {
@@ -149,6 +150,9 @@ final class AutofillPreferences: AutofillPreferencesPersistor {
}
}
+ @UserDefaultsWrapper(key: .autofillSurveyEnabled, defaultValue: true)
+ var autofillSurveyEnabled: Bool
+
private var statisticsStore: StatisticsStore {
return injectedDependencyStore ?? defaultDependencyStore
}
diff --git a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift
index 4f254b3a0f..e7e56043b8 100644
--- a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift
+++ b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift
@@ -17,6 +17,8 @@
//
import Foundation
+import BrowserServicesKit
+import Common
final class AutofillPreferencesModel: ObservableObject {
@@ -59,6 +61,12 @@ final class AutofillPreferencesModel: ObservableObject {
}
}
+ @Published private(set) var autofillSurveyEnabled: Bool {
+ didSet {
+ persistor.autofillSurveyEnabled = autofillSurveyEnabled && Bundle.main.preferredLocalizations.first == "en"
+ }
+ }
+
@MainActor
@Published private(set) var passwordManager: PasswordManager {
didSet {
@@ -153,6 +161,7 @@ final class AutofillPreferencesModel: ObservableObject {
autolockLocksFormFilling = persistor.autolockLocksFormFilling
passwordManager = persistor.passwordManager
hasNeverPromptWebsites = !neverPromptWebsitesManager.neverPromptWebsites.isEmpty
+ autofillSurveyEnabled = persistor.autofillSurveyEnabled
}
private var persistor: AutofillPreferencesPersistor
@@ -192,4 +201,33 @@ final class AutofillPreferencesModel: ObservableObject {
NSWorkspace.shared.open(.fullDiskAccess)
}
+ func launchSurvey(statisticsStore: StatisticsStore = LocalStatisticsStore(),
+ activationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP),
+ operatingSystemVersion: String = ProcessInfo.processInfo.operatingSystemVersion.description,
+ appVersion: String = AppVersion.shared.versionNumber,
+ hardwareModel: String? = HardwareModel.model) {
+
+ let surveyURLBuilder = SurveyURLBuilder(
+ statisticsStore: statisticsStore,
+ operatingSystemVersion: operatingSystemVersion,
+ appVersion: appVersion,
+ hardwareModel: hardwareModel,
+ daysSinceActivation: activationDateStore.daysSinceActivation(),
+ daysSinceLastActive: activationDateStore.daysSinceLastActive()
+ )
+
+ guard let surveyUrl = surveyURLBuilder.buildSurveyURLWithPasswordsCountSurveyParameter(from: "https://selfserve.decipherinc.com/survey/selfserve/32ab/240307") else {
+ return
+ }
+
+ DispatchQueue.main.async {
+ WindowControllersManager.shared.showTab(with: .url(surveyUrl, credential: nil, source: .appOpenUrl))
+ }
+
+ disableAutofillSurvey()
+ }
+
+ func disableAutofillSurvey() {
+ autofillSurveyEnabled = false
+ }
}
diff --git a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift
index cb36a0d227..d3e4501562 100644
--- a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift
+++ b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift
@@ -67,6 +67,43 @@ extension Preferences {
// Autofill Content Button
PreferencePaneSection {
+
+ // New section
+ if model.autofillSurveyEnabled {
+ HStack(alignment: .top, spacing: 20) {
+ Image(.passwordsDDG128)
+ .frame(width: 64, height: 48)
+
+ VStack(alignment: .leading) {
+ Text(verbatim: "Help us improve!")
+ .bold()
+ Text(verbatim: "We want to make using passwords in DuckDuckGo better.")
+ .foregroundColor(.greyText)
+ .padding(.top, 1)
+
+ HStack {
+ Button(action: {
+ model.disableAutofillSurvey()
+ }, label: {
+ Text(verbatim: "No Thanks")
+ })
+ Button(action: {
+ model.launchSurvey()
+ }, label: {
+ Text(verbatim: "Take Survey")
+ })
+ .buttonStyle(DefaultActionButtonStyle(enabled: true))
+ }
+ .padding(.top, 12)
+ }
+
+ Spacer()
+ }
+ .padding()
+ .roundedBorder()
+ .padding(.bottom, 24)
+ }
+
Button(UserText.autofillViewContentButton) {
model.showAutofillPopover()
}
diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift
index 6b804973c4..525f21d8e1 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: "144.0.1"),
+ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1-1"),
.package(path: "../SwiftUIExtensions"),
.package(path: "../XPCHelper"),
],
diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift
index 6aa9f1b8d2..8edcbdcd8a 100644
--- a/LocalPackages/NetworkProtectionMac/Package.swift
+++ b/LocalPackages/NetworkProtectionMac/Package.swift
@@ -31,7 +31,7 @@ let package = Package(
.library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]),
],
dependencies: [
- .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1"),
+ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1-1"),
.package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.1"),
.package(path: "../XPCHelper"),
.package(path: "../SwiftUIExtensions"),
diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift
index 0d69eb5246..d9f3cfbaf7 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: "144.0.1"),
+ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "144.0.1-1"),
.package(path: "../SwiftUIExtensions")
],
targets: [
diff --git a/UnitTests/DataExport/MockSecureVault.swift b/UnitTests/DataExport/MockSecureVault.swift
index d9d55a92fa..5f07ac42f2 100644
--- a/UnitTests/DataExport/MockSecureVault.swift
+++ b/UnitTests/DataExport/MockSecureVault.swift
@@ -58,6 +58,14 @@ final class MockSecureVault: AutofillSecureVault {
return storedAccounts
}
+ func accountsCount() throws -> Int {
+ return storedAccounts.count
+ }
+
+ func accountsCountBucket() throws -> String {
+ return ""
+ }
+
func accountsFor(domain: String) throws -> [SecureVaultModels.WebsiteAccount] {
return storedAccounts.filter { $0.domain == domain }
}
@@ -327,6 +335,10 @@ class MockDatabaseProvider: AutofillDatabaseProvider {
return _accounts
}
+ func accountsCount() throws -> Int {
+ return _accounts.count
+ }
+
func neverPromptWebsites() throws -> [SecureVaultModels.NeverPromptWebsites] {
return _neverPromptWebsites
}
diff --git a/UnitTests/Preferences/AutofillPreferencesModelTests.swift b/UnitTests/Preferences/AutofillPreferencesModelTests.swift
index 9851f65f72..ef02dc27f1 100644
--- a/UnitTests/Preferences/AutofillPreferencesModelTests.swift
+++ b/UnitTests/Preferences/AutofillPreferencesModelTests.swift
@@ -29,6 +29,7 @@ final class AutofillPreferencesPersistorMock: AutofillPreferencesPersistor {
var passwordManager: PasswordManager = .duckduckgo
var autolockLocksFormFilling: Bool = false
var debugScriptEnabled: Bool = false
+ var autofillSurveyEnabled: Bool = false
}
final class UserAuthenticatorMock: UserAuthenticating {