From 94f3cd9439d0de267d22ee3cf7d578492768e417 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Thu, 9 May 2024 16:16:13 +0200 Subject: [PATCH] Add temporary survey to Passwords Settings screen (#2753) Task/Issue URL: https://app.asana.com/0/488551667048375/1206567467430497/f Tech Design URL: CC: Description: Adds temporary survey to the Passwords Settings screen --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++-- .../Passwords-DDG-128.imageset/Contents.json | 12 ++++++ .../Passwords-DDG-128.svg | 31 +++++++++++++++ .../Common/Surveys/SurveyURLBuilder.swift | 27 +++++++++++++ .../Utilities/UserDefaultsWrapper.swift | 1 + DuckDuckGo/Menus/MainMenu.swift | 5 +++ .../Model/AutofillPreferences.swift | 4 ++ .../Model/AutofillPreferencesModel.swift | 38 +++++++++++++++++++ .../View/PreferencesAutofillView.swift | 37 ++++++++++++++++++ .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- UnitTests/DataExport/MockSecureVault.swift | 12 ++++++ .../AutofillPreferencesModelTests.swift | 1 + 15 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Autofill/Passwords-DDG-128.imageset/Passwords-DDG-128.svg 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 {