diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 6f34929142..bfb533e0a8 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -30,6 +30,7 @@ public enum FeatureFlag: String { case autofillPasswordGeneration case autofillOnByDefault case autofillFailureReporting + case autofillOnForExistingUsers case incontextSignup case autoconsentOnByDefault case history @@ -58,6 +59,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .remoteReleasable(.subfeature(AutofillSubfeature.onByDefault)) case .autofillFailureReporting: return .remoteReleasable(.feature(.autofillBreakageReporter)) + case .autofillOnForExistingUsers: + return .remoteReleasable(.subfeature(AutofillSubfeature.onForExistingUsers)) case .incontextSignup: return .remoteReleasable(.feature(.incontextSignup)) case .autoconsentOnByDefault: diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 7c1cc45b99..0239f849ef 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -219,7 +219,12 @@ extension Pixel { case autofillLoginsSaveLoginModalConfirmed case autofillLoginsSaveLoginModalDismissed case autofillLoginsSaveLoginModalExcludeSiteConfirmed - + + case autofillLoginsSaveLoginOnboardingModalDisplayed + case autofillLoginsSaveLoginOnboardingModalConfirmed + case autofillLoginsSaveLoginOnboardingModalDismissed + case autofillLoginsSaveLoginOnboardingModalExcludeSiteConfirmed + case autofillLoginsSavePasswordModalDisplayed case autofillLoginsSavePasswordModalConfirmed case autofillLoginsSavePasswordModalDismissed @@ -246,10 +251,9 @@ extension Pixel { case autofillLoginsFillLoginInlineAuthenticationDeviceAuthCancelled case autofillLoginsAutopromptDismissed - case autofillLoginsFillLoginInlineDisablePromptShown - case autofillLoginsFillLoginInlineDisablePromptAutofillKept - case autofillLoginsFillLoginInlineDisablePromptAutofillDisabled - + case autofillLoginsFillLoginInlineDisableSnackbarShown + case autofillLoginsFillLoginInlineDisableSnackbarOpenSettings + case autofillLoginsSettingsEnabled case autofillLoginsSettingsDisabled case autofillLoginsSettingsResetExcludedDisplayed @@ -946,7 +950,12 @@ extension Pixel.Event { case .autofillLoginsSaveLoginModalConfirmed: return "m_autofill_logins_save_login_inline_confirmed" case .autofillLoginsSaveLoginModalDismissed: return "m_autofill_logins_save_login_inline_dismissed" case .autofillLoginsSaveLoginModalExcludeSiteConfirmed: return "m_autofill_logins_save_login_exclude_site_confirmed" - + + case .autofillLoginsSaveLoginOnboardingModalDisplayed: return "autofill_logins_save_login_inline_onboarding_displayed" + case .autofillLoginsSaveLoginOnboardingModalConfirmed: return "autofill_logins_save_login_inline_onboarding_confirmed" + case .autofillLoginsSaveLoginOnboardingModalDismissed: return "autofill_logins_save_login_inline_onboarding_dismissed" + case .autofillLoginsSaveLoginOnboardingModalExcludeSiteConfirmed: return "autofill_logins_save_login_onboarding_exclude_site_confirmed" + case .autofillLoginsSavePasswordModalDisplayed: return "m_autofill_logins_save_password_inline_displayed" case .autofillLoginsSavePasswordModalConfirmed: return "m_autofill_logins_save_password_inline_confirmed" case .autofillLoginsSavePasswordModalDismissed: return "m_autofill_logins_save_password_inline_dismissed" @@ -979,9 +988,8 @@ extension Pixel.Event { case .autofillLoginsAutopromptDismissed: return "m_autofill_logins_autoprompt_dismissed" - case .autofillLoginsFillLoginInlineDisablePromptShown: return "m_autofill_logins_save_disable-prompt_shown" - case .autofillLoginsFillLoginInlineDisablePromptAutofillKept: return "m_autofill_logins_save_disable-prompt_autofill-kept" - case .autofillLoginsFillLoginInlineDisablePromptAutofillDisabled: return "m_autofill_logins_save_disable-prompt_autofill-disabled" + case .autofillLoginsFillLoginInlineDisableSnackbarShown: return "autofill_logins_save_disable_snackbar_shown" + case .autofillLoginsFillLoginInlineDisableSnackbarOpenSettings: return "autofill_logins_save_disable_snackbar_open_settings" case .autofillLoginsSettingsEnabled: return "m_autofill_logins_settings_enabled" case .autofillLoginsSettingsDisabled: return "m_autofill_logins_settings_disabled" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a330b2bb17..46ddcde01b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -950,7 +950,9 @@ EE4FB1882A28D11900E5CBA7 /* NetworkProtectionStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4FB1872A28D11900E5CBA7 /* NetworkProtectionStatusViewModel.swift */; }; EE50052E29C369D300AE0773 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE50052D29C369D300AE0773 /* FeatureFlag.swift */; }; EE50053029C3BA0800AE0773 /* InternalUserStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE50052F29C3BA0800AE0773 /* InternalUserStore.swift */; }; + EE5929622C5A8AF40029380B /* AutofillUsageMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5929612C5A8AF40029380B /* AutofillUsageMonitor.swift */; }; EE72CA852A862D000043B5B3 /* NetworkProtectionDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE72CA842A862D000043B5B3 /* NetworkProtectionDebugViewController.swift */; }; + EE7623BE2C5D038200FA061C /* MockFeatureFlagger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7623BD2C5D038200FA061C /* MockFeatureFlagger.swift */; }; EE7917912A83DE93008DFF28 /* CombineTestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7917902A83DE93008DFF28 /* CombineTestUtilities.swift */; }; EE7A92872AC6DE4700832A36 /* NetworkProtectionNotificationIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7A92862AC6DE4700832A36 /* NetworkProtectionNotificationIdentifier.swift */; }; EE8594992A44791C008A6D06 /* NetworkProtectionTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE8594982A44791C008A6D06 /* NetworkProtectionTunnelController.swift */; }; @@ -2694,7 +2696,9 @@ EE4FB1872A28D11900E5CBA7 /* NetworkProtectionStatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionStatusViewModel.swift; sourceTree = ""; }; EE50052D29C369D300AE0773 /* FeatureFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; EE50052F29C3BA0800AE0773 /* InternalUserStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalUserStore.swift; sourceTree = ""; }; + EE5929612C5A8AF40029380B /* AutofillUsageMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillUsageMonitor.swift; sourceTree = ""; }; EE72CA842A862D000043B5B3 /* NetworkProtectionDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugViewController.swift; sourceTree = ""; }; + EE7623BD2C5D038200FA061C /* MockFeatureFlagger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFeatureFlagger.swift; sourceTree = ""; }; EE7917902A83DE93008DFF28 /* CombineTestUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineTestUtilities.swift; sourceTree = ""; }; EE7A92862AC6DE4700832A36 /* NetworkProtectionNotificationIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNotificationIdentifier.swift; sourceTree = ""; }; EE8594982A44791C008A6D06 /* NetworkProtectionTunnelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionTunnelController.swift; sourceTree = ""; }; @@ -5613,6 +5617,7 @@ 6F03CB032C32EFA8004179A8 /* MockPixelFiring.swift */, 9F23B8082C2BE9B700950875 /* MockURLOpener.swift */, 9FEA22332C3271DC006B03BF /* MockTimer.swift */, + EE7623BD2C5D038200FA061C /* MockFeatureFlagger.swift */, 9F8007252C5261AF003EDAF4 /* MockPrivacyDataReporter.swift */, 9F69331C2C5A191400CD6A5D /* MockTutorialSettings.swift */, ); @@ -6002,6 +6007,7 @@ C17B59552A03AAC40055F2D1 /* PasswordGeneration */, 31951E9328230D8900CAF535 /* Shared */, F407605428131923006B1E0B /* SaveLogin */, + EE5929612C5A8AF40029380B /* AutofillUsageMonitor.swift */, ); name = Autofill; sourceTree = ""; @@ -7162,6 +7168,7 @@ D66F683D2BB333C100AE93E2 /* SubscriptionContainerView.swift in Sources */, 851B128822200575004781BC /* Onboarding.swift in Sources */, 9FB027192C26BC29009EA190 /* BrowsersComparisonModel.swift in Sources */, + EE5929622C5A8AF40029380B /* AutofillUsageMonitor.swift in Sources */, 3151F0EE2735800800226F58 /* VoiceSearchFeedbackView.swift in Sources */, 37CF91642BB4A82A00BADCAE /* CrashCollectionOnboardingViewModel.swift in Sources */, 6F64AA5D2C4920D200CF4489 /* ShortcutAccessoryView.swift in Sources */, @@ -7513,6 +7520,7 @@ F1BDDBFE2C340D9C00459306 /* SubscriptionFlowViewModelTests.swift in Sources */, F1BDDC022C340DDF00459306 /* SyncManagementViewModelTests.swift in Sources */, D625AAEC2BBEF27600BC189A /* TabURLInterceptorTests.swift in Sources */, + EE7623BE2C5D038200FA061C /* MockFeatureFlagger.swift in Sources */, 5694372B2BE3F2D900C0881B /* SyncErrorHandlerTests.swift in Sources */, 987130C7294AAB9F00AB05E0 /* MenuBookmarksViewModelTests.swift in Sources */, 858650D32469BFAD00C36F8A /* DaxDialogTests.swift in Sources */, @@ -10473,7 +10481,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 181.1.0; + version = 182.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fd7fcfd5de..0e95a916c7 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" : "ff87c19636bce8baa34b7984defa779c083f9ce9", - "version" : "181.1.0" + "revision" : "d67979814bdd3c4c43a38e6694c56f1fdb5969ac", + "version" : "182.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "097e545c737db78cb1d253a87a9acd6dd8ad8497", - "version" : "6.4.0" + "revision" : "f97053d24c21ea301d4067adbbe0899ff940526a", + "version" : "6.7.0" } }, { diff --git a/DuckDuckGo/ActionMessageView.swift b/DuckDuckGo/ActionMessageView.swift index f0a3ef3f63..ca419635ae 100644 --- a/DuckDuckGo/ActionMessageView.swift +++ b/DuckDuckGo/ActionMessageView.swift @@ -90,6 +90,7 @@ class ActionMessageView: UIView { numberOfLines: Int = 0, actionTitle: String? = nil, presentationLocation: PresentationLocation = .withBottomBar(andAddressBarBottom: false), + duration: TimeInterval = Constants.duration, onAction: @escaping () -> Void = {}, onDidDismiss: @escaping () -> Void = {}) { let messageView = loadFromXib() @@ -99,6 +100,7 @@ class ActionMessageView: UIView { message: message.string, actionTitle: actionTitle, presentationLocation: presentationLocation, + duration: duration, onAction: onAction, onDidDismiss: onDidDismiss) } @@ -106,6 +108,7 @@ class ActionMessageView: UIView { static func present(message: String, actionTitle: String? = nil, presentationLocation: PresentationLocation = .withBottomBar(andAddressBarBottom: false), + duration: TimeInterval = Constants.duration, onAction: @escaping () -> Void = {}, onDidDismiss: @escaping () -> Void = {}) { let messageView = loadFromXib() @@ -114,6 +117,7 @@ class ActionMessageView: UIView { message: message, actionTitle: actionTitle, presentationLocation: presentationLocation, + duration: duration, onAction: onAction, onDidDismiss: onDidDismiss) } @@ -122,6 +126,7 @@ class ActionMessageView: UIView { message: String, actionTitle: String? = nil, presentationLocation: PresentationLocation = .withBottomBar(andAddressBarBottom: false), + duration: TimeInterval = Constants.duration, onAction: @escaping () -> Void = {}, onDidDismiss: @escaping () -> Void = {}) { guard let window = UIApplication.shared.firstKeyWindow else { return } @@ -159,7 +164,7 @@ class ActionMessageView: UIView { messageView?.dismissAndFadeOut() } messageView.dismissWorkItem = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + Constants.duration, execute: workItem) + DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: workItem) presentedMessages.append(messageView) } diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 944c12d26c..ac22078290 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -83,6 +83,7 @@ import WebKit private var crashReportUploaderOnboarding: CrashCollectionOnboarding? private var autofillPixelReporter: AutofillPixelReporter? + private var autofillUsageMonitor = AutofillUsageMonitor() var privacyProDataReporter: PrivacyProDataReporting! diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index 58197aa18e..c2261b6d07 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -257,12 +257,18 @@ public class AppUserDefaults: AppSettings { if let isNewInstall = autofillIsNewInstallForOnByDefault, isNewInstall, featureFlagger.isFeatureOn(.autofillOnByDefault) { - autofillCredentialsHasBeenEnabledAutomaticallyIfNecessary = true - autofillCredentialsEnabled = true + enableAutofillCredentials() + } else if featureFlagger.isFeatureOn(.autofillOnForExistingUsers) { + enableAutofillCredentials() } } } - + + private func enableAutofillCredentials() { + autofillCredentialsHasBeenEnabledAutomaticallyIfNecessary = true + autofillCredentialsEnabled = true + } + var autofillCredentialsEnabled: Bool { get { // setAutofillCredentialsEnabledAutomaticallyIfNecessary() used here to automatically turn on autofill for people if: diff --git a/DuckDuckGo/Assets.xcassets/App-DuckDuckGo-32.imageset/App-DuckDuckGo-32.pdf b/DuckDuckGo/Assets.xcassets/App-DuckDuckGo-32.imageset/App-DuckDuckGo-32.pdf new file mode 100644 index 0000000000..99256881e0 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/App-DuckDuckGo-32.imageset/App-DuckDuckGo-32.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/App-DuckDuckGo-32.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/App-DuckDuckGo-32.imageset/Contents.json new file mode 100644 index 0000000000..1ae202a815 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/App-DuckDuckGo-32.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "App-DuckDuckGo-32.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Autofill-Color-24.imageset/Autofill-Color-24.pdf b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Autofill-Color-24.imageset/Autofill-Color-24.pdf new file mode 100644 index 0000000000..92cb050504 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Autofill-Color-24.imageset/Autofill-Color-24.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Autofill-Color-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Autofill-Color-24.imageset/Contents.json new file mode 100644 index 0000000000..b4e82ba339 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Autofill-Color-24.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Autofill-Color-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Lock-Color-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Lock-Color-24.imageset/Contents.json new file mode 100644 index 0000000000..9f9ee57181 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Lock-Color-24.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Lock-Color-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Lock-Color-24.imageset/Lock-Color-24.pdf b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Lock-Color-24.imageset/Lock-Color-24.pdf new file mode 100644 index 0000000000..aaa0822138 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Lock-Color-24.imageset/Lock-Color-24.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Sync-Color-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Sync-Color-24.imageset/Contents.json new file mode 100644 index 0000000000..6fef7213dc --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Sync-Color-24.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Sync-Color-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Sync-Color-24.imageset/Sync-Color-24.pdf b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Sync-Color-24.imageset/Sync-Color-24.pdf new file mode 100644 index 0000000000..80f9255e82 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/Sync-Color-24.imageset/Sync-Color-24.pdf differ diff --git a/DuckDuckGo/AutofillDebugViewController.swift b/DuckDuckGo/AutofillDebugViewController.swift index 41345d0e5e..f194928580 100644 --- a/DuckDuckGo/AutofillDebugViewController.swift +++ b/DuckDuckGo/AutofillDebugViewController.swift @@ -42,6 +42,15 @@ class AutofillDebugViewController: UITableViewController { } } + @UserDefaultsWrapper(key: .autofillSaveModalRejectionCount, defaultValue: 0) + private var autofillSaveModalRejectionCount: Int + + @UserDefaultsWrapper(key: .autofillSaveModalDisablePromptShown, defaultValue: false) + private var autofillSaveModalDisablePromptShown: Bool + + @UserDefaultsWrapper(key: .autofillFirstTimeUser, defaultValue: true) + private var autofillFirstTimeUser: Bool + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) @@ -59,6 +68,10 @@ class AutofillDebugViewController: UITableViewController { eventMapping: EventMapping { _, _, _, _ in }) autofillPixelReporter.resetStoreDefaults() ActionMessageView.present(message: "Autofill Data reset") + autofillSaveModalRejectionCount = 0 + autofillSaveModalDisablePromptShown = false + autofillFirstTimeUser = true + _ = AppDependencyProvider.shared.autofillNeverPromptWebsitesManager.deleteAllNeverPromptWebsites() } else if cell.tag == Row.addAutofillData.rawValue { promptForNumberOfLoginsToAdd() } else if cell.tag == Row.resetEmailProtectionInContextSignUp.rawValue { diff --git a/DuckDuckGo/AutofillLoginListViewModel.swift b/DuckDuckGo/AutofillLoginListViewModel.swift index 45c9097b10..31557e9284 100644 --- a/DuckDuckGo/AutofillLoginListViewModel.swift +++ b/DuckDuckGo/AutofillLoginListViewModel.swift @@ -88,7 +88,7 @@ final class AutofillLoginListViewModel: ObservableObject { private let secureVault: (any AutofillSecureVault)? private let autofillNeverPromptWebsitesManager: AutofillNeverPromptWebsitesManager private let privacyConfig: PrivacyConfiguration - private let breakageReporterKeyValueStoring: KeyValueStoringDictionaryRepresentable + private let keyValueStore: KeyValueStoringDictionaryRepresentable private var cachedDeletedCredentials: SecureVaultModels.WebsiteCredentials? private let autofillDomainNameUrlMatcher = AutofillDomainNameUrlMatcher() private let autofillDomainNameUrlSort = AutofillDomainNameUrlSort() @@ -111,7 +111,7 @@ final class AutofillLoginListViewModel: ObservableObject { } self?.updateData() self?.showBreakageReporter = false - }, keyValueStoring: breakageReporterKeyValueStoring, storageConfiguration: .autofillConfig) + }, keyValueStoring: keyValueStore, storageConfiguration: .autofillConfig) @Published private (set) var viewState: AutofillLoginListViewModel.ViewState = .authLocked @Published private(set) var sections = [AutofillLoginListSectionType]() { @@ -138,6 +138,7 @@ final class AutofillLoginListViewModel: ObservableObject { get { appSettings.autofillCredentialsEnabled } set { appSettings.autofillCredentialsEnabled = newValue + keyValueStore.set(false, forKey: UserDefaultsWrapper.Key.autofillFirstTimeUser.rawValue) NotificationCenter.default.post(name: AppUserDefaults.Notifications.autofillEnabledChange, object: self) } } @@ -149,7 +150,7 @@ final class AutofillLoginListViewModel: ObservableObject { currentTabUid: String? = nil, autofillNeverPromptWebsitesManager: AutofillNeverPromptWebsitesManager = AppDependencyProvider.shared.autofillNeverPromptWebsitesManager, privacyConfig: PrivacyConfiguration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig, - breakageReporterKeyValueStoring: KeyValueStoringDictionaryRepresentable = UserDefaults.standard) { + keyValueStore: KeyValueStoringDictionaryRepresentable = UserDefaults.standard) { self.appSettings = appSettings self.tld = tld self.secureVault = secureVault @@ -157,7 +158,7 @@ final class AutofillLoginListViewModel: ObservableObject { self.currentTabUid = currentTabUid self.autofillNeverPromptWebsitesManager = autofillNeverPromptWebsitesManager self.privacyConfig = privacyConfig - self.breakageReporterKeyValueStoring = breakageReporterKeyValueStoring + self.keyValueStore = keyValueStore if let count = getAccountsCount() { authenticationNotRequired = count == 0 || AppDependencyProvider.shared.autofillLoginSession.isSessionValid diff --git a/DuckDuckGo/AutofillLoginSettingsListViewController.swift b/DuckDuckGo/AutofillLoginSettingsListViewController.swift index 7fc7f8caa4..92f1aa5264 100644 --- a/DuckDuckGo/AutofillLoginSettingsListViewController.swift +++ b/DuckDuckGo/AutofillLoginSettingsListViewController.swift @@ -34,6 +34,7 @@ enum AutofillSettingsSource: String { case homeScreenWidget = "home_screen_widget" case lockScreenWidget = "lock_screen_widget" case newTabPageShortcut = "new_tab_page_shortcut" + case saveLoginDisablePrompt = "save_login_disable_prompt" } protocol AutofillLoginSettingsListViewControllerDelegate: AnyObject { @@ -169,6 +170,7 @@ final class AutofillLoginSettingsListViewController: UIViewController { var selectedAccount: SecureVaultModels.WebsiteAccount? var openSearch: Bool + let source: AutofillSettingsSource init(appSettings: AppSettings, currentTabUrl: URL? = nil, @@ -186,6 +188,7 @@ final class AutofillLoginSettingsListViewController: UIViewController { self.syncService = syncService self.selectedAccount = selectedAccount self.openSearch = openSearch + self.source = source super.init(nibName: nil, bundle: nil) authenticate() @@ -967,7 +970,7 @@ extension AutofillLoginSettingsListViewController: EnableAutofillSettingsTableVi if value { Pixel.fire(pixel: .autofillLoginsSettingsEnabled) } else { - Pixel.fire(pixel: .autofillLoginsSettingsDisabled) + Pixel.fire(pixel: .autofillLoginsSettingsDisabled, withAdditionalParameters: ["source": source.rawValue]) } viewModel.isAutofillEnabledInSettings = value diff --git a/DuckDuckGo/AutofillUsageMonitor.swift b/DuckDuckGo/AutofillUsageMonitor.swift new file mode 100644 index 0000000000..4302aec266 --- /dev/null +++ b/DuckDuckGo/AutofillUsageMonitor.swift @@ -0,0 +1,34 @@ +// +// AutofillUsageMonitor.swift +// DuckDuckGo +// +// 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 Core + +final class AutofillUsageMonitor { + + init() { + NotificationCenter.default.addObserver(self, selector: #selector(didReceiveSaveEvent), name: .autofillSaveEvent, object: nil) + } + + @UserDefaultsWrapper(key: .autofillFirstTimeUser, defaultValue: true) + private var autofillFirstTimeUser: Bool + + @objc private func didReceiveSaveEvent() { + autofillFirstTimeUser = false + } +} diff --git a/DuckDuckGo/AutofillViews.swift b/DuckDuckGo/AutofillViews.swift index 633d2fe261..ffb219210c 100644 --- a/DuckDuckGo/AutofillViews.swift +++ b/DuckDuckGo/AutofillViews.swift @@ -58,8 +58,9 @@ struct AutofillViews { struct AppIconHeader: View { var body: some View { - Image.appIcon - .scaledToFit() + Image(.appDuckDuckGo32) + .resizable() + .frame(width: 48, height: 48) } } diff --git a/DuckDuckGo/SaveLoginView.swift b/DuckDuckGo/SaveLoginView.swift index f2dc2cb311..601441e663 100644 --- a/DuckDuckGo/SaveLoginView.swift +++ b/DuckDuckGo/SaveLoginView.swift @@ -84,23 +84,20 @@ struct SaveLoginView: View { .zIndex(1) VStack { - Spacer() - .frame(height: Const.Size.topPadding) + Spacer(minLength: Const.Size.topPadding) AutofillViews.AppIconHeader() - Spacer() - .frame(height: Const.Size.headlineTopPadding) + Spacer(minLength: Const.Size.contentSpacing) AutofillViews.Headline(title: title) - if #available(iOS 16.0, *) { - contentView - .padding([.top, .bottom], contentPadding) - } else { - Spacer() - contentView - Spacer() + Spacer(minLength: Const.Size.headlineToContentSpacing) + contentView + Spacer(minLength: Const.Size.contentSpacing) + if case .newUser = layoutType { + featuresView.padding([.bottom], Const.Size.featuresListPadding) } ctaView - bottomSpacer } + .padding([.bottom], Const.Size.bodyBottomPadding) + .fixedSize(horizontal: false, vertical: shouldFixSize) .background(GeometryReader { proxy -> Color in DispatchQueue.main.async { viewModel.contentHeight = proxy.size.height } return Color.clear @@ -110,29 +107,90 @@ struct SaveLoginView: View { .padding(.horizontal, horizontalPadding) } + var shouldFixSize: Bool { + AutofillViews.isIPhonePortrait(verticalSizeClass, horizontalSizeClass) || AutofillViews.isIPad(verticalSizeClass, horizontalSizeClass) + } + private func shouldUseScrollView() -> Bool { var useScrollView: Bool = false if #available(iOS 16.0, *) { useScrollView = AutofillViews.contentHeightExceedsScreenHeight(viewModel.contentHeight) } else { - useScrollView = viewModel.contentHeight > frame.height + Const.Size.ios15scrollOffset + useScrollView = viewModel.contentHeight > frame.height } return useScrollView } - private var contentPadding: CGFloat { - if AutofillViews.isIPhonePortrait(verticalSizeClass, horizontalSizeClass) { - return Const.Size.contentSpacerHeight - } else if AutofillViews.isIPad(verticalSizeClass, horizontalSizeClass) { - return Const.Size.contentSpacerHeightIPad - } else { - return Const.Size.contentSpacerHeightLandscape + @ViewBuilder private func featuresListItem(imageResource: ImageResource, title: String, subtitle: String) -> some View { + HStack(alignment: .top, spacing: Const.Size.featuresListItemHorizontalSpacing) { + Image(imageResource).frame(width: Const.Size.featuresListItemImageWidthHeight, height: Const.Size.featuresListItemImageWidthHeight) + VStack(alignment: .leading, spacing: Const.Size.featuresListItemVerticalSpacing) { + Text(title) + .daxSubheadSemibold() + .foregroundColor(Color(designSystemColor: .textPrimary)) + .frame(maxWidth: .infinity, alignment: .topLeading) + Text(subtitle) + .daxSubheadRegular() + .foregroundColor(Color(designSystemColor: .textSecondary)) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + } + .padding(0) + .frame(maxWidth: .infinity, alignment: .topLeading) } + .padding(0) + .frame(maxWidth: .infinity, alignment: .topLeading) } - private var ctaView: some View { + @ViewBuilder private var featuresView: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .center) { + Text(UserText.autofillOnboardingKeyFeaturesTitle) + .font(Font.system(size: 12, weight: .semibold)) + .multilineTextAlignment(.center) + .foregroundColor(Color(designSystemColor: .textSecondary)) + .frame(width: 255, alignment: .top) + } + .padding(.vertical, Const.Size.featuresListVerticalSpacing) + .frame(maxWidth: .infinity, alignment: .center) + Rectangle() + .fill(Color(designSystemColor: .container)) + .frame(height: 1) + VStack(alignment: .leading, spacing: Const.Size.featuresListVerticalSpacing) { + featuresListItem( + imageResource: .autofillColor24, + title: UserText.autofillOnboardingKeyFeaturesSignInsTitle, + subtitle: UserText.autofillOnboardingKeyFeaturesSignInsDescription + ) + featuresListItem( + imageResource: .lockColor24, + title: UserText.autofillOnboardingKeyFeaturesSecureStorageTitle, + subtitle: viewModel.secureStorageDescription + ) + featuresListItem( + imageResource: .syncColor24, + title: UserText.autofillOnboardingKeyFeaturesSyncTitle, + subtitle: UserText.autofillOnboardingKeyFeaturesSyncDescription + ) + } + .padding(.horizontal, Const.Size.featuresListPadding) + .padding(.top, Const.Size.featuresListTopPadding) + .padding(.bottom, Const.Size.featuresListPadding) + } + .padding(0) + .frame(maxWidth: .infinity, alignment: .topLeading) + .cornerRadius(Const.Size.featuresListBorderCornerRadius) + .overlay( + RoundedRectangle(cornerRadius: Const.Size.featuresListBorderCornerRadius) + .inset(by: 0.5) + .stroke(Color(designSystemColor: .container), lineWidth: 1) + ) + .fixedSize(horizontal: false, vertical: true) + } + + @ViewBuilder private var ctaView: some View { VStack(spacing: Const.Size.ctaVerticalSpacing) { AutofillViews.PrimaryButton(title: confirmButton, action: viewModel.save) @@ -154,21 +212,6 @@ struct SaveLoginView: View { } } - private var bottomSpacer: some View { - VStack { - if AutofillViews.isIPhonePortrait(verticalSizeClass, horizontalSizeClass) { - AutofillViews.LegacySpacerView(height: Const.Size.bottomSpacerHeight, legacyHeight: Const.Size.bottomSpacerLegacyHeight) - } else if AutofillViews.isIPad(verticalSizeClass, horizontalSizeClass) { - AutofillViews.LegacySpacerView(height: Const.Size.bottomSpacerHeightIPad, - legacyHeight: orientation == .portrait ? Const.Size.bottomSpacerHeightIPad - : Const.Size.bottomSpacerLegacyHeightIPad) - } else { - AutofillViews.LegacySpacerView(height: Const.Size.bottomSpacerHeight, - legacyHeight: Const.Size.bottomSpacerLegacyHeightLandscape) - } - } - } - @ViewBuilder private var contentView: some View { switch layoutType { @@ -198,17 +241,18 @@ private enum Const { static let closeButtonOffsetPortrait: CGFloat = 44.0 static let closeButtonOffsetPortraitSmallFrame: CGFloat = 16.0 static let topPadding: CGFloat = 56.0 - static let headlineTopPadding: CGFloat = 24.0 - static let ios15scrollOffset: CGFloat = 80.0 - static let contentSpacerHeight: CGFloat = 24.0 - static let contentSpacerHeightIPad: CGFloat = 34.0 - static let contentSpacerHeightLandscape: CGFloat = 44.0 + static let contentSpacing: CGFloat = 24.0 + static let headlineToContentSpacing: CGFloat = 8.0 static let ctaVerticalSpacing: CGFloat = 8.0 - static let bottomSpacerHeight: CGFloat = 12.0 - static let bottomSpacerHeightIPad: CGFloat = 24.0 - static let bottomSpacerLegacyHeight: CGFloat = 16.0 - static let bottomSpacerLegacyHeightIPad: CGFloat = 64.0 - static let bottomSpacerLegacyHeightLandscape: CGFloat = 44.0 + static let bodyBottomPadding: CGFloat = 24.0 + static let featureListItemIconGap: CGFloat = 8.0 + static let featuresListItemImageWidthHeight: CGFloat = 24.0 + static let featuresListItemHorizontalSpacing: CGFloat = 12.0 + static let featuresListItemVerticalSpacing: CGFloat = 2.0 + static let featuresListVerticalSpacing: CGFloat = 12.0 + static let featuresListPadding: CGFloat = 16.0 + static let featuresListTopPadding: CGFloat = 12.0 + static let featuresListBorderCornerRadius: CGFloat = 8.0 } } diff --git a/DuckDuckGo/SaveLoginViewController.swift b/DuckDuckGo/SaveLoginViewController.swift index aee5e5339f..610e8bb0fb 100644 --- a/DuckDuckGo/SaveLoginViewController.swift +++ b/DuckDuckGo/SaveLoginViewController.swift @@ -27,8 +27,7 @@ protocol SaveLoginViewControllerDelegate: AnyObject { func saveLoginViewController(_ viewController: SaveLoginViewController, didUpdateCredentials credentials: SecureVaultModels.WebsiteCredentials) func saveLoginViewControllerDidCancel(_ viewController: SaveLoginViewController) func saveLoginViewController(_ viewController: SaveLoginViewController, didRequestNeverPromptForWebsite domain: String) - func saveLoginViewController(_ viewController: SaveLoginViewController, - didRequestPresentConfirmKeepUsingAlertController alertController: UIAlertController) + func saveLoginViewControllerConfirmKeepUsing(_ viewController: SaveLoginViewController) } class SaveLoginViewController: UIViewController { @@ -71,7 +70,9 @@ class SaveLoginViewController: UIViewController { return } switch viewModel.layoutType { - case .newUser, .saveLogin: + case .newUser: + Pixel.fire(pixel: .autofillLoginsSaveLoginOnboardingModalDismissed) + case .saveLogin: Pixel.fire(pixel: .autofillLoginsSaveLoginModalDismissed) case .savePassword: Pixel.fire(pixel: .autofillLoginsSavePasswordModalDismissed) @@ -95,7 +96,9 @@ class SaveLoginViewController: UIViewController { installChildViewController(controller) switch saveViewModel.layoutType { - case .newUser, .saveLogin: + case .newUser: + Pixel.fire(pixel: .autofillLoginsSaveLoginOnboardingModalDisplayed) + case .saveLogin: Pixel.fire(pixel: .autofillLoginsSaveLoginModalDisplayed) case .savePassword: Pixel.fire(pixel: .autofillLoginsSavePasswordModalDisplayed) @@ -111,7 +114,9 @@ extension SaveLoginViewController: SaveLoginViewModelDelegate { func saveLoginViewModelDidSave(_ viewModel: SaveLoginViewModel) { switch viewModel.layoutType { case .saveLogin, .savePassword, .newUser: - if viewModel.layoutType == .savePassword { + if case .newUser = viewModel.layoutType { + Pixel.fire(pixel: .autofillLoginsSaveLoginOnboardingModalConfirmed) + } else if case .savePassword = viewModel.layoutType { Pixel.fire(pixel: .autofillLoginsSavePasswordModalConfirmed) } else { Pixel.fire(pixel: .autofillLoginsSaveLoginModalConfirmed) @@ -132,44 +137,16 @@ extension SaveLoginViewController: SaveLoginViewModelDelegate { } func saveLoginViewModelNeverPrompt(_ viewModel: SaveLoginViewModel) { - Pixel.fire(pixel: .autofillLoginsSaveLoginModalExcludeSiteConfirmed) + if case .newUser = viewModel.layoutType { + Pixel.fire(pixel: .autofillLoginsSaveLoginOnboardingModalExcludeSiteConfirmed) + } else { + Pixel.fire(pixel: .autofillLoginsSaveLoginModalExcludeSiteConfirmed) + } delegate?.saveLoginViewController(self, didRequestNeverPromptForWebsite: viewModel.accountDomain) } - func saveLoginViewModelConfirmKeepUsing(_ viewModel: SaveLoginViewModel, isAlreadyDismissed: Bool) { - - let isSelfPresentingAlert = !isAlreadyDismissed - - let alertController = UIAlertController(title: UserText.autofillKeepEnabledAlertTitle, - message: UserText.autofillKeepEnabledAlertMessage, - preferredStyle: .alert) - - let disableAction = UIAlertAction(title: UserText.autofillKeepEnabledAlertDisableAction, style: .cancel) { _ in - Pixel.fire(pixel: .autofillLoginsFillLoginInlineDisablePromptAutofillDisabled) - if isSelfPresentingAlert { - self.delegate?.saveLoginViewControllerDidCancel(self) - } - AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled = false - } - - let keepUsingAction = UIAlertAction(title: UserText.autofillKeepEnabledAlertKeepUsingAction, style: .default) { _ in - Pixel.fire(pixel: .autofillLoginsFillLoginInlineDisablePromptAutofillKept) - if isSelfPresentingAlert { - self.delegate?.saveLoginViewControllerDidCancel(self) - } - } - - alertController.addAction(disableAction) - alertController.addAction(keepUsingAction) - - alertController.preferredAction = keepUsingAction - - if isAlreadyDismissed { - delegate?.saveLoginViewController(self, didRequestPresentConfirmKeepUsingAlertController: alertController) - } else { - Pixel.fire(pixel: .autofillLoginsFillLoginInlineDisablePromptShown) - present(alertController, animated: true) - } + func saveLoginViewModelConfirmKeepUsing(_ viewModel: SaveLoginViewModel) { + delegate?.saveLoginViewControllerConfirmKeepUsing(self) } func saveLoginViewModelDidResizeContent(_ viewModel: SaveLoginViewModel, contentHeight: CGFloat) { diff --git a/DuckDuckGo/SaveLoginViewModel.swift b/DuckDuckGo/SaveLoginViewModel.swift index 1586627994..d8adba63dc 100644 --- a/DuckDuckGo/SaveLoginViewModel.swift +++ b/DuckDuckGo/SaveLoginViewModel.swift @@ -20,12 +20,13 @@ import UIKit import BrowserServicesKit import Core +import LocalAuthentication protocol SaveLoginViewModelDelegate: AnyObject { func saveLoginViewModelDidSave(_ viewModel: SaveLoginViewModel) func saveLoginViewModelDidCancel(_ viewModel: SaveLoginViewModel) func saveLoginViewModelNeverPrompt(_ viewModel: SaveLoginViewModel) - func saveLoginViewModelConfirmKeepUsing(_ viewModel: SaveLoginViewModel, isAlreadyDismissed: Bool) + func saveLoginViewModelConfirmKeepUsing(_ viewModel: SaveLoginViewModel) func saveLoginViewModelDidResizeContent(_ viewModel: SaveLoginViewModel, contentHeight: CGFloat) } @@ -54,7 +55,8 @@ final class SaveLoginViewModel: ObservableObject { private let maximumPasswordDisplayCount = 40 private let credentialManager: SaveAutofillLoginManagerProtocol private let appSettings: AppSettings - + private let biometryType: LABiometryType + private var dismissButtonWasPressed = false var didSave = false @@ -98,6 +100,21 @@ final class SaveLoginViewModel: ObservableObject { credentialManager.username } + var secureStorageDescription: String { + let biometryString: String + + switch biometryType { + case .touchID: + biometryString = UserText.autofillOnboardingKeyFeaturesSecureStorageDescriptionParameterTouchID + case .faceID: + biometryString = UserText.autofillOnboardingKeyFeaturesSecureStorageDescriptionParameterFaceID + default: + biometryString = UserText.autofillOnboardingKeyFeaturesSecureStorageDescriptionParameterPasscode + } + + return UserText.autofillOnboardingKeyFeaturesSecureStorageDescription(biometryString: biometryString) + } + var usernameTruncated: String { AutofillInterfaceEmailTruncator.truncateEmail(credentialManager.username, maxLength: 36) } @@ -128,11 +145,16 @@ final class SaveLoginViewModel: ObservableObject { private var attributedLayoutType: SaveLoginView.LayoutType? - internal init(credentialManager: SaveAutofillLoginManagerProtocol, appSettings: AppSettings, layoutType: SaveLoginView.LayoutType? = nil, domainLastShownOn: String? = nil) { + internal init(credentialManager: SaveAutofillLoginManagerProtocol, + appSettings: AppSettings, + layoutType: SaveLoginView.LayoutType? = nil, + domainLastShownOn: String? = nil, + biometryType: LABiometryType = LAContext().biometryType) { self.credentialManager = credentialManager self.appSettings = appSettings self.attributedLayoutType = layoutType self.domainLastShownOn = domainLastShownOn + self.biometryType = biometryType } private func updateRejectionCountIfNeeded() { @@ -143,7 +165,7 @@ final class SaveLoginViewModel: ObservableObject { autofillSaveModalRejectionCount += 1 } - private func shouldShowAutofillKeepUsingConfirmation() -> Bool { + private func shouldShowDisableAutofillPrompt() -> Bool { if autofillSaveModalDisablePromptShown || !autofillFirstTimeUser { return false } @@ -152,14 +174,10 @@ final class SaveLoginViewModel: ObservableObject { private func cancel() { updateRejectionCountIfNeeded() - if shouldShowAutofillKeepUsingConfirmation() { - delegate?.saveLoginViewModelConfirmKeepUsing(self, isAlreadyDismissed: !dismissButtonWasPressed) - autofillSaveModalDisablePromptShown = true - } else { - delegate?.saveLoginViewModelDidCancel(self) - } + delegate?.saveLoginViewModelDidCancel(self) + showDisableAutofillPromptIfNeeded() } - + func cancelButtonPressed() { dismissButtonWasPressed = true cancel() @@ -184,7 +202,16 @@ final class SaveLoginViewModel: ObservableObject { func neverPrompt() { didSave = true - autofillFirstTimeUser = false + updateRejectionCountIfNeeded() delegate?.saveLoginViewModelNeverPrompt(self) + showDisableAutofillPromptIfNeeded() + } + + private func showDisableAutofillPromptIfNeeded() { + if shouldShowDisableAutofillPrompt() { + delegate?.saveLoginViewModelConfirmKeepUsing(self) + autofillSaveModalDisablePromptShown = true + autofillFirstTimeUser = false + } } } diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 92327ec970..8275e7d1e2 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -2841,10 +2841,20 @@ extension TabViewController: SaveLoginViewControllerDelegate { } } - func saveLoginViewController(_ viewController: SaveLoginViewController, - didRequestPresentConfirmKeepUsingAlertController alertController: UIAlertController) { - Pixel.fire(pixel: .autofillLoginsFillLoginInlineDisablePromptShown) - present(alertController, animated: true) + func saveLoginViewControllerConfirmKeepUsing(_ viewController: SaveLoginViewController) { + Pixel.fire(pixel: .autofillLoginsFillLoginInlineDisableSnackbarShown) + DispatchQueue.main.async { + let addressBarBottom = self.appSettings.currentAddressBarPosition.isBottom + ActionMessageView.present(message: UserText.autofillDisablePromptMessage, + actionTitle: UserText.autofillDisablePromptAction, + presentationLocation: .withBottomBar(andAddressBarBottom: addressBarBottom), + duration: 4.0, + onAction: { [weak self] in + Pixel.fire(pixel: .autofillLoginsFillLoginInlineDisableSnackbarOpenSettings) + guard let mainVC = self?.view.window?.rootViewController as? MainViewController else { return } + mainVC.launchAutofillLogins(source: .saveLoginDisablePrompt) + }) + } } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 21d50699ba..1bb196c82b 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -391,20 +391,34 @@ public struct UserText { public static let emptyDownloads = NSLocalizedString("downloads.downloads-list.empty", value: "No files downloaded yet", comment: "Empty downloads list placholder") - public static let autofillSaveLoginTitleNewUser = NSLocalizedString("autofill.save-login.new-user.title", value: "Do you want DuckDuckGo to save your password?", comment: "Title displayed on modal asking for the user to save the login for the first time") - public static let autofillSaveLoginTitle = NSLocalizedString("autofill.save-login.title", value: "Save Password?", comment: "Title displayed on modal asking for the user to save the login") + public static let autofillSaveLoginTitleNewUser = NSLocalizedString("autofill.save-login.new-user.title", value: "Save this password?", comment: "Title displayed on modal asking for the user to save the login for the first time") + public static let autofillSaveLoginTitle = NSLocalizedString("autofill.save-login.title", value: "Save password?", comment: "Title displayed on modal asking for the user to save the login") public static let autofillUpdateUsernameTitle = NSLocalizedString("autofill.update-usernamr.title", value: "Update username?", comment: "Title displayed on modal asking for the user to update the username") - public static let autofillSaveLoginMessageNewUser = NSLocalizedString("autofill.save-login.new-user.message", value: "Passwords are stored securely on your device.", comment: "Message displayed on modal asking for the user to save the login for the first time") - public static let autofillSaveLoginNotNowCTA = NSLocalizedString("autofill.save-login.not-now.CTA", value: "Don’t Save", comment: "Cancel CTA displayed on modal asking for the user to save the login") - public static let autofillSaveLoginNeverPromptCTA = NSLocalizedString("autofill.save-login.never-prompt.CTA", value:"Never Ask for This Site", comment: "CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin") - + public static let autofillSaveLoginMessageNewUser = NSLocalizedString("autofill.save-login.new-user.message", value: "DuckDuckGo Passwords & Autofill stores passwords securely on your device.", comment: "Message displayed on modal asking for the user to save the login for the first time") + public static let autofillSaveLoginNeverPromptCTA = NSLocalizedString("autofill.save-login.never-prompt.CTA", value: "Never Ask for This Site", comment: "CTA displayed on modal asking if the user never wants to be prompted to save a login for this website agin") + public static func autofillUpdatePassword(for title: String) -> String { let message = NSLocalizedString("autofill.update-password.title", value: "Update password for\n%@?", comment: "Title displayed on modal asking for the user to update the password") return message.format(arguments: title) } public static let autoUpdatePasswordMessage = NSLocalizedString("autofill.update-password.message", value: "DuckDuckGo will update this stored password on your device.", comment: "Message displayed on modal asking for the user to update the password") - + + public static let autofillOnboardingKeyFeaturesTitle = NSLocalizedString("autofill.onboarding.key-features.title", value: "Key Features", comment: "Title of autofill onboarding prompt's features list") + public static let autofillOnboardingKeyFeaturesSignInsTitle = NSLocalizedString("autofill.onboarding.key-features.sign-ins.title", value: "Seamless sign-ins", comment: "Title of autofill onboarding prompt's sign-in feature") + public static let autofillOnboardingKeyFeaturesSignInsDescription = NSLocalizedString("autofill.onboarding.key-features.sign-ins.description", value: "No need to remember login info.", comment: "Description of autofill onboarding prompt's sign-in feature") + public static let autofillOnboardingKeyFeaturesSecureStorageTitle = NSLocalizedString("autofill.onboarding.key-features.secure-storage.title", value: "Secure storage", comment: "Title of autofill onboarding prompt's secure storage feature") + static func autofillOnboardingKeyFeaturesSecureStorageDescription(biometryString: String) -> String { + let localized = NSLocalizedString("autofill.onboarding.key-features.secure-storage.description", value: "Passwords are encrypted, stored on device, and locked with %@.", comment: "Description of autofill onboarding prompt's secure storage feature with a string describing the available biometry + passcode as a parameter") + return String(format: localized, biometryString) + } + public static let autofillOnboardingKeyFeaturesSecureStorageDescriptionParameterFaceID = NSLocalizedString("autofill.onboarding.key-features.secure-storage.description.parameter.face-id", value: "Face ID or passcode", comment: "Parameter for the description of autofill onboarding prompt's secure storage feature describing Face ID biometry + passcode") + public static let autofillOnboardingKeyFeaturesSecureStorageDescriptionParameterTouchID = NSLocalizedString("autofill.onboarding.key-features.secure-storage.description.parameter.touch-id", value: "Touch ID or passcode", comment: "Parameter for the description of autofill onboarding prompt's secure storage feature describing Touch ID biometry + passcode") + public static let autofillOnboardingKeyFeaturesSecureStorageDescriptionParameterPasscode = NSLocalizedString("autofill.onboarding.key-features.secure-storage.description.parameter.passcode", value: "passcode", comment: "Parameter for the description of autofill onboarding prompt's secure storage feature describing passcode only if no biometry are available") + public static let autofillOnboardingKeyFeaturesSecureStorageDescription = NSLocalizedString("autofill.onboarding.key-features.secure-storage.description", value: "Passwords are encrypted, stored on device, and locked with Face ID or passcode.", comment: "Description of autofill onboarding prompt's secure storage feature") + public static let autofillOnboardingKeyFeaturesSyncTitle = NSLocalizedString("autofill.onboarding.key-features.sync.title", value: "Sync between devices", comment: "Title of autofill onboarding prompt's sync feature") + public static let autofillOnboardingKeyFeaturesSyncDescription = NSLocalizedString("autofill.onboarding.key-features.sync.description", value: "End-to-end encrypted and easy to set up when you’re ready.", comment: "Description of autofill onboarding prompt's sync feature") + public static let autofillSavePasswordSaveCTA = NSLocalizedString("autofill.save-password.save.CTA", value: "Save Password", comment: "Confirm CTA displayed on modal asking for the user to save the password") public static let autofillUpdatePasswordSaveCTA = NSLocalizedString("autofill.update-password.save.CTA", value: "Update Password", comment: "Confirm CTA displayed on modal asking for the user to update the password") public static let autofillShowPassword = NSLocalizedString("autofill.show-password", value: "Show Password", comment: "Accessibility title for a Show Password button displaying actial password instead of *****") @@ -414,11 +428,11 @@ public struct UserText { public static let autofillLoginUpdatedToastMessage = NSLocalizedString("autofill.login-updated.toast", value: "Password updated", comment: "Message displayed after updating an autofill login") public static let autofillLoginSaveToastActionButton = NSLocalizedString("autofill.login-save-action-button.toast", value: "View", comment: "Button displayed after saving/updating an autofill login that takes the user to the saved login") - public static let autofillKeepEnabledAlertTitle = NSLocalizedString("autofill.keep-enabled.alert.title", value: "Do you want to keep saving passwords?", comment: "Title for alert when asking the user if they want to keep using autofill") - public static let autofillKeepEnabledAlertMessage = NSLocalizedString("autofill.keep-enabled.alert.message", value: "You can disable this at any time in Settings.", comment: "Message for alert when asking the user if they want to keep using autofill") - public static let autofillKeepEnabledAlertKeepUsingAction = NSLocalizedString("autofill.keep-enabled.alert.keep-using", value: "Keep Saving", comment: "Confirm action for alert when asking the user if they want to keep using autofill") public static let autofillKeepEnabledAlertDisableAction = NSLocalizedString("autofill.keep-enabled.alert.disable", value: "Disable", comment: "Disable action for alert when asking the user if they want to keep using autofill") + public static let autofillDisablePromptMessage = NSLocalizedString("autofill.disable.prompt.message", value: "You can turn off password saving anytime.", comment: "Message for informing user that they can disable autofill in Settings") + public static let autofillDisablePromptAction = NSLocalizedString("autofill.disable.prompt.action.open-settings", value: "Open Settings", comment: "Open Settings action for disabling autofill in Settings") + public static let actionAutofillLogins = NSLocalizedString("action.title.autofill.logins", value: "Passwords", comment: "Autofill Logins menu item opening the login list") // MARK: - Waitlist diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index b2fb25888e..d3a756fef8 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -271,6 +271,12 @@ /* Do not translate - stringsdict entry */ "autofill.delete.all.passwords.sync.confirmation.body" = "autofill.delete.all.passwords.sync.confirmation.body"; +/* Open Settings action for disabling autofill in Settings */ +"autofill.disable.prompt.action.open-settings" = "Open Settings"; + +/* Message for informing user that they can disable autofill in Settings */ +"autofill.disable.prompt.message" = "You can turn off password saving anytime."; + /* Text link to email protection website */ "autofill.enable.email.protection" = "Enable Email Protection"; @@ -316,15 +322,6 @@ /* Disable action for alert when asking the user if they want to keep using autofill */ "autofill.keep-enabled.alert.disable" = "Disable"; -/* Confirm action for alert when asking the user if they want to keep using autofill */ -"autofill.keep-enabled.alert.keep-using" = "Keep Saving"; - -/* Message for alert when asking the user if they want to keep using autofill */ -"autofill.keep-enabled.alert.message" = "You can disable this at any time in Settings."; - -/* Title for alert when asking the user if they want to keep using autofill */ -"autofill.keep-enabled.alert.title" = "Do you want to keep saving passwords?"; - /* Button displayed after saving/updating an autofill login that takes the user to the saved login */ "autofill.login-save-action-button.toast" = "View"; @@ -505,6 +502,37 @@ /* Do not translate - stringsdict entry */ "autofill.number.of.passwords" = "autofill.number.of.passwords"; +/* Description of autofill onboarding prompt's secure storage feature + Description of autofill onboarding prompt's secure storage feature with a string describing the available biometry + passcode as a parameter */ +"autofill.onboarding.key-features.secure-storage.description" = "Passwords are encrypted, stored on device, and locked with %@."; + +/* Parameter for the description of autofill onboarding prompt's secure storage feature describing Face ID biometry + passcode */ +"autofill.onboarding.key-features.secure-storage.description.parameter.face-id" = "Face ID or passcode"; + +/* Parameter for the description of autofill onboarding prompt's secure storage feature describing passcode only if no biometry are available */ +"autofill.onboarding.key-features.secure-storage.description.parameter.passcode" = "passcode"; + +/* Parameter for the description of autofill onboarding prompt's secure storage feature describing Touch ID biometry + passcode */ +"autofill.onboarding.key-features.secure-storage.description.parameter.touch-id" = "Touch ID or passcode"; + +/* Title of autofill onboarding prompt's secure storage feature */ +"autofill.onboarding.key-features.secure-storage.title" = "Secure storage"; + +/* Description of autofill onboarding prompt's sign-in feature */ +"autofill.onboarding.key-features.sign-ins.description" = "No need to remember login info."; + +/* Title of autofill onboarding prompt's sign-in feature */ +"autofill.onboarding.key-features.sign-ins.title" = "Seamless sign-ins"; + +/* Description of autofill onboarding prompt's sync feature */ +"autofill.onboarding.key-features.sync.description" = "End-to-end encrypted and easy to set up when you’re ready."; + +/* Title of autofill onboarding prompt's sync feature */ +"autofill.onboarding.key-features.sync.title" = "Sync between devices"; + +/* Title of autofill onboarding prompt's features list */ +"autofill.onboarding.key-features.title" = "Key Features"; + /* Subtitle for prompt to use suggested strong password for creating a login */ "autofill.password-generation-prompt.subtitle" = "Passwords are stored securely on your device."; @@ -551,16 +579,13 @@ "autofill.save-login.never-prompt.CTA" = "Never Ask for This Site"; /* Message displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.message" = "Passwords are stored securely on your device."; +"autofill.save-login.new-user.message" = "DuckDuckGo Passwords & Autofill stores passwords securely on your device."; /* Title displayed on modal asking for the user to save the login for the first time */ -"autofill.save-login.new-user.title" = "Do you want DuckDuckGo to save your password?"; - -/* Cancel CTA displayed on modal asking for the user to save the login */ -"autofill.save-login.not-now.CTA" = "Don’t Save"; +"autofill.save-login.new-user.title" = "Save this password?"; /* Title displayed on modal asking for the user to save the login */ -"autofill.save-login.title" = "Save Password?"; +"autofill.save-login.title" = "Save password?"; /* Confirm CTA displayed on modal asking for the user to save the password */ "autofill.save-password.save.CTA" = "Save Password"; diff --git a/DuckDuckGoTests/AppUserDefaultsTests.swift b/DuckDuckGoTests/AppUserDefaultsTests.swift index a523960d40..1fdc34fa43 100644 --- a/DuckDuckGoTests/AppUserDefaultsTests.swift +++ b/DuckDuckGoTests/AppUserDefaultsTests.swift @@ -144,7 +144,7 @@ class AppUserDefaultsTests: XCTestCase { appUserDefaults.autofillCredentialsHasBeenEnabledAutomaticallyIfNecessary = false appUserDefaults.autofillCredentialsSavePromptShowAtLeastOnce = false appUserDefaults.autofillIsNewInstallForOnByDefault = true - let featureFlagger = createFeatureFlagger(withSubfeatureEnabled: true) + let featureFlagger = createFeatureFlagger(withFeatureFlagEnabled: .autofillOnByDefault) appUserDefaults.featureFlagger = featureFlagger XCTAssertTrue(appUserDefaults.autofillCredentialsEnabled) @@ -166,6 +166,28 @@ class AppUserDefaultsTests: XCTestCase { XCTAssertEqual(appUserDefaults.autofillCredentialsEnabled, false) } + func testWhenAutofillCredentialsIsDisabledAndHasNotBeenTurnedOnAutomaticallyBeforeAndPromptHasNotBeenSeenAndAllUsersFeatureFlagEnabledThenDefaultAutofillStateIsTrue() { + let appUserDefaults = AppUserDefaults(groupName: testGroupName) + appUserDefaults.autofillCredentialsHasBeenEnabledAutomaticallyIfNecessary = false + appUserDefaults.autofillCredentialsSavePromptShowAtLeastOnce = false + appUserDefaults.autofillIsNewInstallForOnByDefault = false + let featureFlagger = createFeatureFlagger(withFeatureFlagEnabled: .autofillOnForExistingUsers) + appUserDefaults.featureFlagger = featureFlagger + + XCTAssertTrue(appUserDefaults.autofillCredentialsEnabled) + } + + func testWhenAutofillCredentialsIsDisabledAndHasNotBeenTurnedOnAutomaticallyBeforeAndPromptHasBeenSeenAndAllUsersFeatureFlagEnabledThenDefaultAutofillStateIsFalse() { + let appUserDefaults = AppUserDefaults(groupName: testGroupName) + appUserDefaults.autofillCredentialsHasBeenEnabledAutomaticallyIfNecessary = false + appUserDefaults.autofillCredentialsSavePromptShowAtLeastOnce = true + appUserDefaults.autofillIsNewInstallForOnByDefault = false + let featureFlagger = createFeatureFlagger(withFeatureFlagEnabled: .autofillOnForExistingUsers) + appUserDefaults.featureFlagger = featureFlagger + + XCTAssertFalse(appUserDefaults.autofillCredentialsEnabled) + } + func testDefaultAutoconsentStateIsFalse_WhenNotInRollout() { let appUserDefaults = AppUserDefaults(groupName: testGroupName) appUserDefaults.featureFlagger = createFeatureFlagger(withSubfeatureEnabled: false) @@ -174,7 +196,7 @@ class AppUserDefaultsTests: XCTestCase { func testDefaultAutoconsentStateIsTrue_WhenInRollout() { let appUserDefaults = AppUserDefaults(groupName: testGroupName) - appUserDefaults.featureFlagger = createFeatureFlagger(withSubfeatureEnabled: true) + appUserDefaults.featureFlagger = createFeatureFlagger(withFeatureFlagEnabled: .autoconsentOnByDefault) XCTAssertTrue(appUserDefaults.autoconsentEnabled) } @@ -183,7 +205,7 @@ class AppUserDefaultsTests: XCTestCase { // When setting disabled by user and rollout enabled appUserDefaults.autoconsentEnabled = false - appUserDefaults.featureFlagger = createFeatureFlagger(withSubfeatureEnabled: true) + appUserDefaults.featureFlagger = createFeatureFlagger(withFeatureFlagEnabled: .autoconsentOnByDefault) XCTAssertFalse(appUserDefaults.autoconsentEnabled) @@ -212,5 +234,10 @@ class AppUserDefaultsTests: XCTestCase { return mockPrivacyConfiguration } - + + private func createFeatureFlagger(withFeatureFlagEnabled featureFlag: FeatureFlag) -> FeatureFlagger { + let mockFeatureFlagger = MockFeatureFlagger() + mockFeatureFlagger.enabledFeatureFlags.append(featureFlag) + return mockFeatureFlagger + } } diff --git a/DuckDuckGoTests/AutofillLoginListViewModelTests.swift b/DuckDuckGoTests/AutofillLoginListViewModelTests.swift index 2c164b931b..b32d8bcbe9 100644 --- a/DuckDuckGoTests/AutofillLoginListViewModelTests.swift +++ b/DuckDuckGoTests/AutofillLoginListViewModelTests.swift @@ -386,7 +386,7 @@ class AutofillLoginListViewModelTests: XCTestCase { currentTabUid: "1", autofillNeverPromptWebsitesManager: manager, privacyConfig: makePrivacyConfig(from: configDisabled), - breakageReporterKeyValueStoring: MockKeyValueStore()) + keyValueStore: MockKeyValueStore()) XCTAssertFalse(model.shouldShowBreakageReporter()) } @@ -405,7 +405,7 @@ class AutofillLoginListViewModelTests: XCTestCase { currentTabUid: "1", autofillNeverPromptWebsitesManager: manager, privacyConfig: makePrivacyConfig(from: configEnabled), - breakageReporterKeyValueStoring: MockKeyValueStore()) + keyValueStore: MockKeyValueStore()) XCTAssertFalse(model.shouldShowBreakageReporter()) } @@ -424,7 +424,7 @@ class AutofillLoginListViewModelTests: XCTestCase { currentTabUid: "1", autofillNeverPromptWebsitesManager: manager, privacyConfig: makePrivacyConfig(from: configEnabled), - breakageReporterKeyValueStoring: MockKeyValueStore()) + keyValueStore: MockKeyValueStore()) XCTAssertFalse(model.shouldShowBreakageReporter()) } @@ -443,7 +443,7 @@ class AutofillLoginListViewModelTests: XCTestCase { currentTabUid: "1", autofillNeverPromptWebsitesManager: manager, privacyConfig: makePrivacyConfig(from: configEnabled), - breakageReporterKeyValueStoring: MockKeyValueStore()) + keyValueStore: MockKeyValueStore()) XCTAssertFalse(model.shouldShowBreakageReporter()) } @@ -463,7 +463,7 @@ class AutofillLoginListViewModelTests: XCTestCase { currentTabUid: "1", autofillNeverPromptWebsitesManager: manager, privacyConfig: makePrivacyConfig(from: configEnabled), - breakageReporterKeyValueStoring: MockKeyValueStore()) + keyValueStore: MockKeyValueStore()) let identifier = currentTabUrl!.privacySafeDomainIdentifier model.breakageReporter.persistencyManager.set(value: "2024-07-16", forKey: identifier!, expiryDate: Date()) @@ -486,7 +486,7 @@ class AutofillLoginListViewModelTests: XCTestCase { currentTabUid: "1", autofillNeverPromptWebsitesManager: manager, privacyConfig: makePrivacyConfig(from: configEnabled), - breakageReporterKeyValueStoring: MockKeyValueStore()) + keyValueStore: MockKeyValueStore()) XCTAssertTrue(model.shouldShowBreakageReporter()) } @@ -506,7 +506,7 @@ class AutofillLoginListViewModelTests: XCTestCase { currentTabUid: "1", autofillNeverPromptWebsitesManager: manager, privacyConfig: makePrivacyConfig(from: configEnabled), - breakageReporterKeyValueStoring: MockKeyValueStore()) + keyValueStore: MockKeyValueStore()) let identifier = currentTabUrl!.privacySafeDomainIdentifier model.breakageReporter.persistencyManager.set(value: "2024-01-01", forKey: identifier!, expiryDate: Date()) diff --git a/DuckDuckGoTests/MockFeatureFlagger.swift b/DuckDuckGoTests/MockFeatureFlagger.swift new file mode 100644 index 0000000000..1d2c8d642b --- /dev/null +++ b/DuckDuckGoTests/MockFeatureFlagger.swift @@ -0,0 +1,36 @@ +// +// MockFeatureFlagger.swift +// DuckDuckGo +// +// 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 BrowserServicesKit +import Core + +final class MockFeatureFlagger: FeatureFlagger { + var enabledFeatureFlags: [FeatureFlag] = [] + var enabledFeatureFlag: FeatureFlag? + + func isFeatureOn(forProvider provider: F) -> Bool where F: BrowserServicesKit.FeatureFlagSourceProviding { + guard let flag = provider as? FeatureFlag else { + return false + } + guard enabledFeatureFlags.contains(flag) else { + return false + } + return true + } +} diff --git a/DuckDuckGoTests/Subscription/PrivacyProDataReporterTests.swift b/DuckDuckGoTests/Subscription/PrivacyProDataReporterTests.swift index e9458fe215..65597fd1a0 100644 --- a/DuckDuckGoTests/Subscription/PrivacyProDataReporterTests.swift +++ b/DuckDuckGoTests/Subscription/PrivacyProDataReporterTests.swift @@ -225,9 +225,3 @@ class MockCalendar { date } } - -struct MockFeatureFlagger: FeatureFlagger { - func isFeatureOn(forProvider: F) -> Bool where F: FeatureFlagSourceProviding { - false - } -}