diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7ffe09eba4..35c330c1b6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2136,7 +2136,6 @@ 561D66682B95C45A008ACC5C /* Suggestion.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 561D66692B95C45A008ACC5C /* Suggestion.storyboard */; }; 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */; }; 562984712AC469E400AC20EB /* SyncPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */; }; - 56373AA32B95C2560037DBDD /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56373AA22B95C2560037DBDD /* InfoPlist.xcstrings */; }; 56534DED29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56534DEC29DF252C00121467 /* CapturingDefaultBrowserProvider.swift */; }; 56534DEE29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56534DEC29DF252C00121467 /* CapturingDefaultBrowserProvider.swift */; }; 565E46E02B2725DD0013AC2A /* CriticalPathsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565E46DF2B2725DD0013AC2A /* CriticalPathsTests.swift */; }; @@ -2168,7 +2167,6 @@ 56D145F229E6F06D00E3488A /* MockBookmarkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */; }; 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; 56D6A3D729DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; - 56DB51622B95C2B9000F9CAF /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56DB51612B95C2B9000F9CAF /* InfoPlist.xcstrings */; }; 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */; }; 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */; }; @@ -3083,9 +3081,24 @@ BBDFDC5A2B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; BBDFDC5C2B2B8D7000F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; BBDFDC5D2B2B8E2100F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; + C13909EF2B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */; }; + C13909F02B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */; }; + C13909F12B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */; }; + C13909F42B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */; }; + C13909F52B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */; }; + C13909FB2B861039001626ED /* AutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909FA2B861039001626ED /* AutofillActionPresenter.swift */; }; + C13909FC2B861039001626ED /* AutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909FA2B861039001626ED /* AutofillActionPresenter.swift */; }; + C13909FD2B861039001626ED /* AutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909FA2B861039001626ED /* AutofillActionPresenter.swift */; }; C168B9AC2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */; }; C168B9AD2B31DC7F001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */; }; C168B9AE2B31DC7F001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */; }; + C1E961EB2B879E79001760E1 /* MockAutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */; }; + C1E961ED2B879ED9001760E1 /* MockAutofillActionExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E961EC2B879ED9001760E1 /* MockAutofillActionExecutor.swift */; }; + C1E961EF2B87AA29001760E1 /* AutofillActionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E961EE2B87AA29001760E1 /* AutofillActionBuilder.swift */; }; + C1E961F02B87AA29001760E1 /* AutofillActionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E961EE2B87AA29001760E1 /* AutofillActionBuilder.swift */; }; + C1E961F22B87AA29001760E1 /* AutofillActionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E961EE2B87AA29001760E1 /* AutofillActionBuilder.swift */; }; + C1E961F32B87B273001760E1 /* MockAutofillActionExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E961EC2B879ED9001760E1 /* MockAutofillActionExecutor.swift */; }; + C1E961F42B87B276001760E1 /* MockAutofillActionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */; }; CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */; }; CB24F70D29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */; }; CB6BCDF927C6BEFF00CC76DC /* PrivacyFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB6BCDF827C6BEFF00CC76DC /* PrivacyFeatures.swift */; }; @@ -3809,7 +3822,6 @@ 5603D90529B7B746007F9F01 /* MockTabViewItemDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTabViewItemDelegate.swift; sourceTree = ""; }; 561D66692B95C45A008ACC5C /* Suggestion.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Suggestion.storyboard; sourceTree = ""; }; 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPreferencesTests.swift; sourceTree = ""; }; - 56373AA22B95C2560037DBDD /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 56534DEC29DF252C00121467 /* CapturingDefaultBrowserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturingDefaultBrowserProvider.swift; sourceTree = ""; }; 565E46DD2B2725DC0013AC2A /* SyncE2EUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SyncE2EUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 565E46DF2B2725DD0013AC2A /* CriticalPathsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalPathsTests.swift; sourceTree = ""; }; @@ -3827,7 +3839,6 @@ 56D145ED29E6DAD900E3488A /* DataImportProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportProviderTests.swift; sourceTree = ""; }; 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBookmarkManager.swift; sourceTree = ""; }; 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueSetUpView.swift; sourceTree = ""; }; - 56DB51612B95C2B9000F9CAF /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowManager.swift; sourceTree = ""; }; 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTransparentProxyProvider.swift; sourceTree = ""; }; 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; @@ -4465,7 +4476,13 @@ B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = ""; }; BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionSubscriptionEventHandler.swift; sourceTree = ""; }; BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionExternalWaitlistPixels.swift; sourceTree = ""; }; + C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionExecutor.swift; sourceTree = ""; }; + C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillDeleteAllPasswordsExecutorTests.swift; sourceTree = ""; }; + C13909FA2B861039001626ED /* AutofillActionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionPresenter.swift; sourceTree = ""; }; C168B9AB2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillNeverPromptWebsitesManager.swift; sourceTree = ""; }; + C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillActionPresenter.swift; sourceTree = ""; }; + C1E961EC2B879ED9001760E1 /* MockAutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAutofillActionExecutor.swift; sourceTree = ""; }; + C1E961EE2B87AA29001760E1 /* AutofillActionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionBuilder.swift; sourceTree = ""; }; CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationURLProvider.swift; sourceTree = ""; }; CB6BCDF827C6BEFF00CC76DC /* PrivacyFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyFeatures.swift; sourceTree = ""; }; CBDD5DE229A67F2700832877 /* MockConfigurationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockConfigurationStore.swift; sourceTree = ""; }; @@ -5462,7 +5479,6 @@ 4B4BEC312A11B509001D9AC5 /* DuckDuckGoNotifications */ = { isa = PBXGroup; children = ( - 56DB515F2B95C2B9000F9CAF /* Resources */, 7B5291882A1697680022E406 /* Info.plist */, 4B4BEC382A11B509001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift */, 4B4BEC322A11B509001D9AC5 /* Logging.swift */, @@ -6209,20 +6225,15 @@ path = Mocks; sourceTree = ""; }; - 56DB515F2B95C2B9000F9CAF /* Resources */ = { - isa = PBXGroup; - children = ( - 56DB51612B95C2B9000F9CAF /* InfoPlist.xcstrings */, - ); - path = Resources; - sourceTree = ""; - }; 7B1E819A27C8874900FF0E60 /* Autofill */ = { isa = PBXGroup; children = ( 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */, 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */, 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */, + C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */, + C1E961EE2B87AA29001760E1 /* AutofillActionBuilder.swift */, + C13909FA2B861039001626ED /* AutofillActionPresenter.swift */, ); path = Autofill; sourceTree = ""; @@ -6293,7 +6304,6 @@ 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */ = { isa = PBXGroup; children = ( - 56373AA22B95C2560037DBDD /* InfoPlist.xcstrings */, 7BDA36EA2B7E037200AD5388 /* Info.plist */, 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */, ); @@ -6902,6 +6912,7 @@ AA585D93248FD31400E9A3E2 /* UnitTests */ = { isa = PBXGroup; children = ( + C13909F22B85FD60001626ED /* Autofill */, 5629846D2AC460DF00AC20EB /* Sync */, B6A5A28C25B962CB00AA7ADA /* App */, 85F1B0C725EF9747004792B6 /* AppDelegate */, @@ -8308,6 +8319,32 @@ path = View; sourceTree = ""; }; + C13909F22B85FD60001626ED /* Autofill */ = { + isa = PBXGroup; + children = ( + C1E961E62B879E2A001760E1 /* Mocks */, + C13909F72B85FF76001626ED /* Tests */, + ); + path = Autofill; + sourceTree = ""; + }; + C13909F72B85FF76001626ED /* Tests */ = { + isa = PBXGroup; + children = ( + C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */, + ); + path = Tests; + sourceTree = ""; + }; + C1E961E62B879E2A001760E1 /* Mocks */ = { + isa = PBXGroup; + children = ( + C1E961E72B879E4D001760E1 /* MockAutofillActionPresenter.swift */, + C1E961EC2B879ED9001760E1 /* MockAutofillActionExecutor.swift */, + ); + path = Mocks; + sourceTree = ""; + }; CB6BCDF727C689FE00CC76DC /* Resources */ = { isa = PBXGroup; children = ( @@ -9270,7 +9307,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 56DB51622B95C2B9000F9CAF /* InfoPlist.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9292,7 +9328,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 56373AA32B95C2560037DBDD /* InfoPlist.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9751,6 +9786,7 @@ 3706FA7F293F65D500E42796 /* TabIndex.swift in Sources */, 3706FA80293F65D500E42796 /* TabLazyLoaderDataSource.swift in Sources */, 3706FA81293F65D500E42796 /* LoginImport.swift in Sources */, + C13909FC2B861039001626ED /* AutofillActionPresenter.swift in Sources */, 4BF97AD72B43C53D00EB4240 /* NetworkProtectionIPCTunnelController.swift in Sources */, EEC4A66E2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 3706FA83293F65D500E42796 /* LazyLoadable.swift in Sources */, @@ -10412,12 +10448,14 @@ 3706FC7E293F65D500E42796 /* MainWindowController.swift in Sources */, 3706FC7F293F65D500E42796 /* Tab.swift in Sources */, 3706FC81293F65D500E42796 /* DispatchQueueExtensions.swift in Sources */, + C13909F02B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */, 3706FC82293F65D500E42796 /* PermissionAuthorizationPopover.swift in Sources */, 3706FC83293F65D500E42796 /* PopoverMessageViewController.swift in Sources */, 4BF97ADA2B43C5DC00EB4240 /* VPNFeedbackCategory.swift in Sources */, 9D9AE86E2AA76D1F0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, 3707C721294B5D2900682A9F /* WKMenuItemIdentifier.swift in Sources */, 3706FEBE293F6EFF00E42796 /* BWMessageIdGenerator.swift in Sources */, + C1E961F02B87AA29001760E1 /* AutofillActionBuilder.swift in Sources */, 3706FC85293F65D500E42796 /* ShadowView.swift in Sources */, 3706FC86293F65D500E42796 /* FeedbackSender.swift in Sources */, 3706FC88293F65D500E42796 /* TabBarViewItem.swift in Sources */, @@ -10545,6 +10583,7 @@ 3706FE1C293F661700E42796 /* ConnectBitwardenViewModelTests.swift in Sources */, 4B9DB0552A983B55000927DB /* MockWaitlistStorage.swift in Sources */, 3706FE1D293F661700E42796 /* PixelStoreTests.swift in Sources */, + C13909F52B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */, 857E44642A9F70F200ED77A7 /* CampaignVariantTests.swift in Sources */, 3706FE1E293F661700E42796 /* GeolocationProviderTests.swift in Sources */, 9F39106A2B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */, @@ -10654,6 +10693,7 @@ 3706FE6A293F661700E42796 /* FirefoxKeyReaderTests.swift in Sources */, 3706FE6B293F661700E42796 /* AppKitPrivateMethodsAvailabilityTests.swift in Sources */, 3706FE6D293F661700E42796 /* ChromiumBookmarksReaderTests.swift in Sources */, + C1E961F42B87B276001760E1 /* MockAutofillActionPresenter.swift in Sources */, 3706FE6E293F661700E42796 /* FirefoxBookmarksReaderTests.swift in Sources */, 4B9DB05B2A983B55000927DB /* MockWaitlistRequest.swift in Sources */, 028904212A7B25770028369C /* AppConfigurationURLProviderTests.swift in Sources */, @@ -10668,6 +10708,7 @@ 56D145EF29E6DAD900E3488A /* DataImportProviderTests.swift in Sources */, 569277C529DEE09D00B633EF /* ContinueSetUpModelTests.swift in Sources */, 3706FE76293F661700E42796 /* MockSecureVault.swift in Sources */, + C1E961F32B87B273001760E1 /* MockAutofillActionExecutor.swift in Sources */, 376E2D2729428353001CD31B /* BrokenSiteReportingReferenceTests.swift in Sources */, 3707C72F294B5D4F00682A9F /* WebViewTests.swift in Sources */, 5682C69429B79B57004DE3C8 /* TabBarViewItemTests.swift in Sources */, @@ -10942,11 +10983,13 @@ 4B9579602AC7AE700062CA31 /* WKWebView+Download.swift in Sources */, 4B9579612AC7AE700062CA31 /* TabShadowConfig.swift in Sources */, 4B9579622AC7AE700062CA31 /* URLSessionExtension.swift in Sources */, + C13909FD2B861039001626ED /* AutofillActionPresenter.swift in Sources */, 4B9579632AC7AE700062CA31 /* WKWebsiteDataStoreExtension.swift in Sources */, 4B9579642AC7AE700062CA31 /* WindowDraggingView.swift in Sources */, 4B9579652AC7AE700062CA31 /* SecureVaultSorting.swift in Sources */, 4B9579662AC7AE700062CA31 /* PreferencesSidebarModel.swift in Sources */, 4B9579672AC7AE700062CA31 /* DuckPlayerURLExtension.swift in Sources */, + C1E961F22B87AA29001760E1 /* AutofillActionBuilder.swift in Sources */, 4B41EDB72B169887001EEDF4 /* VPNFeedbackFormView.swift in Sources */, 4B9579682AC7AE700062CA31 /* BWEncryptionOutput.m in Sources */, 4B9579692AC7AE700062CA31 /* PermissionState.swift in Sources */, @@ -11255,6 +11298,7 @@ 4B957A7F2AC7AE700062CA31 /* NSApplicationExtension.swift in Sources */, 4B957A802AC7AE700062CA31 /* NSWindowExtension.swift in Sources */, 4B957A812AC7AE700062CA31 /* KeychainType+ClientDefault.swift in Sources */, + C13909F12B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */, 4B41EDA52B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, 4B957A822AC7AE700062CA31 /* SyncDebugMenu.swift in Sources */, 4B957A832AC7AE700062CA31 /* AddBookmarkPopover.swift in Sources */, @@ -12150,6 +12194,7 @@ B6A5A27125B9377300AA7ADA /* StatePersistenceService.swift in Sources */, B68458B025C7E76A00DC17B6 /* WindowManager+StateRestoration.swift in Sources */, B68458C525C7EA0C00DC17B6 /* TabCollection+NSSecureCoding.swift in Sources */, + C13909EF2B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */, 4BB88B5B25B7BA50006F6B06 /* Instruments.swift in Sources */, 9812D895276CEDA5004B6181 /* ContentBlockerRulesLists.swift in Sources */, 4B0511E2262CAA8600F6079C /* NSViewControllerExtension.swift in Sources */, @@ -12165,6 +12210,7 @@ BB5789722B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift in Sources */, B693954F26F04BEB0015B914 /* PaddedImageButton.swift in Sources */, 4BA1A6B8258B081600F6F690 /* EncryptionKeyStoring.swift in Sources */, + C13909FB2B861039001626ED /* AutofillActionPresenter.swift in Sources */, B65783E725F8AAFB00D8DB33 /* String+Punycode.swift in Sources */, B657841A25FA484B00D8DB33 /* NSException+Catch.m in Sources */, B684592F25C93FBF00DC17B6 /* AppStateRestorationManager.swift in Sources */, @@ -12314,6 +12360,7 @@ AA6820EB25503D6A005ED0D5 /* Fire.swift in Sources */, 3158B1492B0BF73000AF130C /* DBPHomeViewController.swift in Sources */, 37445F9C2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */, + C1E961EF2B87AA29001760E1 /* AutofillActionBuilder.swift in Sources */, B6AAAC3E26048F690029438D /* RandomAccessCollectionExtension.swift in Sources */, 4B9292AF26670F5300AD2C21 /* NSOutlineViewExtensions.swift in Sources */, AA585D82248FD31100E9A3E2 /* AppDelegate.swift in Sources */, @@ -12434,6 +12481,7 @@ 37534CA52811987D002621E7 /* AdjacentItemEnumeratorTests.swift in Sources */, B6DA44232616CABC00DD1EC2 /* PixelArgumentsTests.swift in Sources */, 56B234BF2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */, + C13909F42B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */, 37534C9E28104D9B002621E7 /* TabLazyLoaderTests.swift in Sources */, B6619EF62B10DFF700CD9186 /* InstructionsFormatParserTests.swift in Sources */, 569277C429DEE09D00B633EF /* ContinueSetUpModelTests.swift in Sources */, @@ -12569,6 +12617,7 @@ B68172AE269EB43F006D1092 /* GeolocationServiceTests.swift in Sources */, B6AE74342609AFCE005B9B1A /* ProgressEstimationTests.swift in Sources */, B6619F032B17123200CD9186 /* DataImportViewModelTests.swift in Sources */, + C1E961EB2B879E79001760E1 /* MockAutofillActionPresenter.swift in Sources */, 4BA1A6FE258C5C1300F6F690 /* EncryptedValueTransformerTests.swift in Sources */, 85F69B3C25EDE81F00978E59 /* URLExtensionTests.swift in Sources */, B6DA44112616C0FC00DD1EC2 /* PixelTests.swift in Sources */, @@ -12650,6 +12699,7 @@ B63ED0D826AE729600A9DAD1 /* PermissionModelTests.swift in Sources */, B69B504B2726CA2900758A2B /* MockStatisticsStore.swift in Sources */, 37CD54BD27F2ECAE00F1F7B9 /* AutofillPreferencesModelTests.swift in Sources */, + C1E961ED2B879ED9001760E1 /* MockAutofillActionExecutor.swift in Sources */, 37D23789288009CF00BCE03B /* TabCollectionViewModelTests+PinnedTabs.swift in Sources */, 1D3B1AC62937A478006F4388 /* BWRequestTests.swift in Sources */, B630793A26731F2600DCEE41 /* FileDownloadManagerTests.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fc276c3779..42e890f9d9 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -165,7 +165,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", "version" : "1.2.2" diff --git a/DuckDuckGo/Autofill/AutofillActionBuilder.swift b/DuckDuckGo/Autofill/AutofillActionBuilder.swift new file mode 100644 index 0000000000..de48f8d6b3 --- /dev/null +++ b/DuckDuckGo/Autofill/AutofillActionBuilder.swift @@ -0,0 +1,45 @@ +// +// AutofillActionBuilder.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import BrowserServicesKit +import Foundation + +/// Conforming types provide methods to build an `AutofillActionExecutor` and an `AutofillActionPresenter` +protocol AutofillActionBuilder { + func buildExecutor() -> AutofillActionExecutor? + func buildPresenter() -> AutofillActionPresenter +} + +extension AutofillActionBuilder { + func buildPresenter() -> AutofillActionPresenter { + DefaultAutofillActionPresenter() + } +} + +/// Builds an `AutofillActionExecutor` +struct AutofillDeleteAllPasswordsBuilder: AutofillActionBuilder { + @MainActor + func buildExecutor() -> AutofillActionExecutor? { + guard let secureVault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared), + let syncService = NSApp.delegateTyped.syncService else { return nil } + + return AutofillDeleteAllPasswordsExecutor(userAuthenticator: DeviceAuthenticator.shared, + secureVault: secureVault, + syncService: syncService) + } +} diff --git a/DuckDuckGo/Autofill/AutofillActionExecutor.swift b/DuckDuckGo/Autofill/AutofillActionExecutor.swift new file mode 100644 index 0000000000..f574c7e974 --- /dev/null +++ b/DuckDuckGo/Autofill/AutofillActionExecutor.swift @@ -0,0 +1,77 @@ +// +// AutofillActionExecutor.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import BrowserServicesKit +import DDGSync +import AppKit + +/// Conforming types provide an `execute` method which performs some action on autofill types (e.g delete all passwords) +protocol AutofillActionExecutor { + init(userAuthenticator: UserAuthenticating, secureVault: any AutofillSecureVault, syncService: DDGSyncing) + /// NSAlert to display asking a user to confirm the action + var confirmationAlert: NSAlert { get } + /// NSAlert to display when the action is complete + var completionAlert: NSAlert { get } + /// Executes the action + func execute(_ onSuccess: (() -> Void)?) +} + +/// Concrete `AutofillActionExecutor` for deletion of all autofill passwords +struct AutofillDeleteAllPasswordsExecutor: AutofillActionExecutor { + + var confirmationAlert: NSAlert { + let accounts = (try? secureVault.accounts()) ?? [] + return NSAlert.deleteAllPasswordsConfirmationAlert(count: accounts.count, syncEnabled: syncEnabled) + } + + var completionAlert: NSAlert { + let accounts = (try? secureVault.accounts()) ?? [] + return NSAlert.deleteAllPasswordsCompletionAlert(count: accounts.count, syncEnabled: syncEnabled) + } + + private var syncEnabled: Bool { + syncService.authState != .inactive + } + + private var userAuthenticator: UserAuthenticating + private var secureVault: any AutofillSecureVault + private var syncService: DDGSyncing + + init(userAuthenticator: UserAuthenticating, secureVault: any AutofillSecureVault, syncService: DDGSyncing) { + self.userAuthenticator = userAuthenticator + self.secureVault = secureVault + self.syncService = syncService + } + + func execute(_ onSuccess: (() -> Void)? = nil) { + userAuthenticator.authenticateUser(reason: .deleteAllPasswords) { authenticationResult in + guard authenticationResult.authenticated else { return } + + do { + try secureVault.deleteAllWebsiteCredentials() + syncService.scheduler.notifyDataChanged() + onSuccess?() + } catch { + Pixel.fire(.debug(event: .secureVaultError, error: error)) + } + + return + } + } +} diff --git a/DuckDuckGo/Autofill/AutofillActionPresenter.swift b/DuckDuckGo/Autofill/AutofillActionPresenter.swift new file mode 100644 index 0000000000..c759d18944 --- /dev/null +++ b/DuckDuckGo/Autofill/AutofillActionPresenter.swift @@ -0,0 +1,63 @@ +// +// AutofillActionPresenter.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import AppKit + +/// Conforming types handles presentation of `NSAlert`s associated with an `AutofillActionExecutor` +protocol AutofillActionPresenter { + func show(actionExecutor: AutofillActionExecutor, completion: @escaping () -> Void) +} + +/// Handles presentation of an alert associated with an `AutofillActionExecutor` +struct DefaultAutofillActionPresenter: AutofillActionPresenter { + + @MainActor + func show(actionExecutor: AutofillActionExecutor, completion: @escaping () -> Void) { + guard let window else { return } + + let confirmationAlert = actionExecutor.confirmationAlert + let completionAlert = actionExecutor.completionAlert + + confirmationAlert.beginSheetModal(for: window) { response in + switch response { + case .alertFirstButtonReturn: + actionExecutor.execute { + completion() + show(completionAlert) + } + default: + break + } + } + } +} + +private extension DefaultAutofillActionPresenter { + + @MainActor + func show(_ alert: NSAlert) { + guard let window else { return } + alert.beginSheetModal(for: window) + } + + @MainActor + var window: NSWindow? { + WindowControllersManager.shared.lastKeyMainWindowController?.window + } +} diff --git a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift index 2c7e660d36..8aa901b548 100644 --- a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift @@ -228,5 +228,4 @@ extension NSAlert { continuation.resume(returning: self.runModal()) } } - } diff --git a/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift b/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift index 8ce6626df5..627a6fe1f3 100644 --- a/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift +++ b/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift @@ -40,6 +40,7 @@ final class DeviceAuthenticator: UserAuthenticating { case unlockLogins case exportLogins case syncSettings + case deleteAllPasswords var localizedDescription: String { switch self { @@ -48,6 +49,7 @@ final class DeviceAuthenticator: UserAuthenticating { case .unlockLogins: return UserText.pmAutoLockPromptUnlockLogins case .exportLogins: return UserText.pmAutoLockPromptExportLogins case .syncSettings: return UserText.syncAutoLockPrompt + case .deleteAllPasswords: return UserText.deleteAllPasswordsPermissionText } } } @@ -154,7 +156,8 @@ final class DeviceAuthenticator: UserAuthenticating { func authenticateUser(reason: AuthenticationReason, result: @escaping (DeviceAuthenticationResult) -> Void) { let needsAuthenticationForCreditCardsAutofill = reason == .autofillCreditCards && isCreditCardTimeIntervalExpired() let needsAuthenticationForSyncSettings = reason == .syncSettings && isSyncSettingsTimeIntervalExpired() - guard needsAuthenticationForCreditCardsAutofill || needsAuthenticationForSyncSettings || requiresAuthentication else { + let needsAuthenticationForDeleteAllPasswords = reason == .deleteAllPasswords + guard needsAuthenticationForCreditCardsAutofill || needsAuthenticationForSyncSettings || needsAuthenticationForDeleteAllPasswords || requiresAuthentication else { result(.success) return } diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 7d9703d813..9347c6977d 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -3770,6 +3770,486 @@ } } }, + "autofill.items.delete-all-passwords" : { + "comment" : "Opens Delete All Passwords dialog", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Passwörter löschen ..." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Delete All Passwords…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar todas las contraseñas..." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimer tous les mots de passe…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elimina tutte le password..." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle wachtwoorden verwijderen ..." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuń wszystkie hasła" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar todas as palavras-passe…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалить все пароли..." + } + } + } + }, + "autofill.items.delete-all-passwords-completion-button-texy" : { + "comment" : "Button text on dialog confirming deletion was completed", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schließen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Close" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fermer" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chiudi" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sluiten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zamknij" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fechar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрыть" + } + } + } + }, + "autofill.items.delete-all-passwords-completion-message-text" : { + "comment" : "Message displayed on completion of multiple password deletion", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Passwörter gelöscht (%d)" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "All passwords deleted (%d)" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se han eliminado todas las contraseñas (%d)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tous les mots de passe sont supprimés (%d)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tutte le password sono state eliminate (%d)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle wachtwoorden verwijderd (%d)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usunięto wszystkie hasła (%d)" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todas as palavras-passe eliminadas (%d)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Все пароли (%d) удалены" + } + } + } + }, + "autofill.items.delete-all-passwords-confirmation-message-text" : { + "comment" : "Message displayed on dialog asking user to confirm deletion of all passwords", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Möchtest du wirklich alle Passwörter (%d) löschen?" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Are you sure you want to delete all passwords (%d)?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Seguro que quieres borrar todas las contraseñas (%d)?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voulez-vous vraiment supprimer tous les mots de passe (%d) ?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vuoi davvero eliminare tutte le password (%d)?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weet je zeker dat je alle %d wachtwoorden wilt verwijderen?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Czy na pewno chcesz usunąć wszystkie hasła (%d)?" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tens a certeza de que pretendes eliminar todas as palavras-passe (%d)?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы точно хотите удалить все пароли (%d)?" + } + } + } + }, + "autofill.items.delete-all-passwords-device-confirmation-information-text" : { + "comment" : "Information message displayed when deleting all passwords on a device", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine Passwörter werden von diesem Gerät gelöscht. Vergewissere dich, dass du weiterhin eine Möglichkeit hast, auf deine Konten zuzugreifen." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Your passwords will be deleted from this device. Make sure you still have a way to access your accounts." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tus contraseñas se borrarán de este dispositivo. Asegúrate de seguir teniendo una forma de acceder a tus cuentas." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vos mots de passe seront supprimés de cet appareil. Assurez-vous d'avoir toujours un moyen d'accéder à vos comptes." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le tue password verranno eliminate da questo dispositivo. Assicurati di poter ancora accedere ai tuoi account." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je wachtwoorden worden van dit apparaat verwijderd. Zorg ervoor dat je nog steeds toegang hebt tot je accounts." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje hasła zostaną usunięte z tego urządzenia. Upewnij się, że nadal masz możliwość dostępu do swoich kont." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "As tuas palavras-passe serão apagadas deste dispositivo. Certifica-te de que continuas a ter forma de aceder às tuas contas." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароли будут удалены с данного устройства. Убедитесь, что вы по-прежнему можете войти в свои учетные записи." + } + } + } + }, + "autofill.items.delete-all-passwords-permisson-text" : { + "comment" : "Message displayed in system authentication dialog", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Passwörter löschen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "delete all passwords" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar todas las contraseñas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimer tous les mots de passe" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elimina tutte le password" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle wachtwoorden verwijderen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuń wszystkie hasła" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar todas as palavras-passe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалить все пароли" + } + } + } + }, + "autofill.items.delete-all-passwords-synced-completion-information-text" : { + "comment" : "Information message displayed on completion of multiple password deletion when devices are synced", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine Passwörter wurden von allen synchronisierten Geräten gelöscht." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Your passwords have been deleted from all synced devices." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tus contraseñas se han borrado de todos los dispositivos sincronizados." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vos mots de passe ont été supprimés de tous les appareils synchronisés." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le password sono state eliminate da tutti i dispositivi sincronizzati." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je wachtwoorden zijn verwijderd van alle gesynchroniseerde apparaten." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje hasła zostaną usunięte ze wszystkich zsynchronizowanych urządzeń." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "As tuas palavras-passe foram eliminadas de todos os dispositivos sincronizados." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваши пароли были удалены со всех синхронизированных устройств." + } + } + } + }, + "autofill.items.delete-all-passwords-synced-confirmation-information-text" : { + "comment" : "Information message displayed when deleting all passwords on a synced device", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine Passwörter werden von allen synchronisierten Geräten gelöscht. Vergewissere dich, dass du weiterhin eine Möglichkeit hast, auf deine Konten zuzugreifen." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Your passwords will be deleted from all synced devices. Make sure you still have a way to access your accounts." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tus contraseñas se borrarán en todos los dispositivos sincronizados. Asegúrate de seguir teniendo una forma de acceder a tus cuentas." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vos mots de passe seront supprimés de tous les appareils synchronisés. Assurez-vous d'avoir toujours un moyen d'accéder à vos comptes." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le password verranno eliminate da tutti i dispositivi sincronizzati. Assicurati di avere ancora la possibilità di accedere ai tuoi account." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je wachtwoorden worden verwijderd van alle gesynchroniseerde apparaten. Zorg ervoor dat je nog steeds toegang hebt tot je accounts." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje hasła zostaną usunięte ze wszystkich zsynchronizowanych urządzeń. Upewnij się, że nadal masz możliwość dostępu do swoich kont." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "As tuas palavras-passe serão eliminadas de todos os dispositivos sincronizados. Confirma que ainda tens uma forma de aceder às contas." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароли будут удалены со всех синхронизированных устройств. Убедитесь, что вы по-прежнему можете войти в свои учетные записи." + } + } + } + }, "autofill.lock-when-idle" : { "comment" : "Autofill auto-lock setting", "extractionState" : "extracted_with_value", @@ -9887,7 +10367,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Начать с заглавной" + "value" : "С заглавной буквы" } } } @@ -11998,7 +12478,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Разработчик" + "value" : "Разработка" } } } @@ -12959,11 +13439,59 @@ "comment" : "Number of bytes out of total bytes downloaded (1Mb of 2Mb)", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ von %2$@" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@ of %2$@" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ de %2$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ sur %2$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ of %2$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ van %2$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ z %2$@" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ de %2$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ из %2$@" + } } } }, @@ -13751,11 +14279,59 @@ "comment" : "Checkbox to open a Download Manager popover when downloads are completed", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Download-Bedienfeld automatisch öffnen, wenn Downloads abgeschlossen sind" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Automatically open the Downloads panel when downloads complete" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir automáticamente el panel Descargas cuando se completen las descargas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir automatiquement le volet Téléchargements une fois les téléchargements terminés" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri automaticamente il pannello Download al termine dei download" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deelvenster 'Downloads' automatisch openen wanneer het downloaden is voltooid" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatycznie otwieraj panel Pobieranie po zakończeniu pobierania" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir automaticamente o painel Transferências quando as transferências terminarem" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматически открывать панель «Загрузки» по завершении скачивания" + } } } }, @@ -13943,11 +14519,59 @@ "comment" : "Download speed format (1Mb/sec)", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/s" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%@/s" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/s" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/s" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/s" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/sec." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/s" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/s" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/с" + } } } }, @@ -19169,7 +19793,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Помощь" + "value" : "Справка" } } } @@ -24301,7 +24925,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Скрыть другие службы" + "value" : "Скрыть остальные" } } } @@ -24361,7 +24985,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Предпочтения…" + "value" : "Настройки…" } } } @@ -24421,7 +25045,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Закрыть DuckDuckGo" + "value" : "Завершить DuckDuckGo" } } } @@ -24601,7 +25225,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Редактировать" + "value" : "Правка" } } } @@ -25201,7 +25825,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Вставить и подогнать стиль" + "value" : "Вставить и согласовать стиль" } } } @@ -25681,7 +26305,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Функции работы с текстом" + "value" : "Замены" } } } @@ -26281,7 +26905,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Открыть местоположение..." + "value" : "Открыть адрес…" } } } @@ -26634,7 +27258,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Преобразовать в строчные" + "value" : "Строчные" } } } @@ -26687,7 +27311,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Преобразовать в заглавные" + "value" : "Прописные" } } } @@ -41320,7 +41944,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Несколько слов о DuckDuckGo" + "value" : "О приложении DuckDuckGo" } } } @@ -43240,7 +43864,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Распечатать…" + "value" : "Печать…" } } } @@ -48767,7 +49391,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Трансформации" + "value" : "Преобразования" } } } @@ -49286,7 +49910,7 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Просмотр" + "value" : "Вид" } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift index a93e6a5ae3..bfe5ff0d0c 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift @@ -83,25 +83,32 @@ extension Preferences { #if !APPSTORE // SECTION 1: Password Manager PreferencePaneSection(UserText.autofillPasswordManager) { - VStack(alignment: .leading, spacing: 6) { - Picker(selection: passwordManagerBinding, content: { + passwordManagerPicker(passwordManagerBinding) { Text(UserText.autofillPasswordManagerDuckDuckGo).tag(PasswordManager.duckduckgo) + } + } + + if model.passwordManager != .bitwarden { + VStack { + Button(UserText.importPasswords) { + model.openImportBrowserDataWindow() + } + Button(UserText.exportLogins) { + model.openExportLogins() + } + } + .padding(.leading, 15) + } + + VStack(alignment: .leading, spacing: 6) { + passwordManagerPicker(passwordManagerBinding) { Text(UserText.autofillPasswordManagerBitwarden).tag(PasswordManager.bitwarden) - }, label: {}) - .pickerStyle(.radioGroup) - .offset(x: PreferencesViews.Const.pickerHorizontalOffset) + } if model.passwordManager == .bitwarden && !model.isBitwardenSetupFlowPresented { bitwardenStatusView(for: bitwardenManager.status) } } - Spacer() - Button(UserText.importPasswords) { - model.openImportBrowserDataWindow() - } - Button(UserText.exportLogins) { - model.openExportLogins() - } } #endif @@ -168,6 +175,15 @@ extension Preferences { } } + @ViewBuilder + private func passwordManagerPicker(_ binding: Binding, @ViewBuilder content: @escaping () -> some View) -> some View { + Picker(selection: binding, content: { + content() + }, label: {}) + .pickerStyle(.radioGroup) + .offset(x: PreferencesViews.Const.pickerHorizontalOffset) + } + // swiftlint:disable cyclomatic_complexity // swiftlint:disable function_body_length @ViewBuilder private func bitwardenStatusView(for status: BWStatus) -> some View { diff --git a/DuckDuckGo/SecureVault/Extensions/NSAlert+PasswordManager.swift b/DuckDuckGo/SecureVault/Extensions/NSAlert+PasswordManager.swift index 49d5a92a3f..bed25df593 100644 --- a/DuckDuckGo/SecureVault/Extensions/NSAlert+PasswordManager.swift +++ b/DuckDuckGo/SecureVault/Extensions/NSAlert+PasswordManager.swift @@ -79,4 +79,38 @@ extension NSAlert { return alert } + static func deleteAllPasswordsConfirmationAlert(count: Int, syncEnabled: Bool) -> NSAlert { + let messageText = UserText.deleteAllPasswordsConfirmationMessageText(count: count) + let informationText = UserText.deleteAllPasswordsConfirmationInformationText(syncEnabled: syncEnabled) + return autofillActionConfirmationAlert(messageText: messageText, + informationText: informationText, + confirmButtonText: UserText.passwordManagerAlerDeleteButton) + } + + private static func autofillActionConfirmationAlert(messageText: String, + informationText: String, + confirmButtonText: String) -> NSAlert { + let alert = NSAlert() + alert.messageText = messageText + alert.informativeText = informationText + alert.alertStyle = .warning + alert.addButton(withTitle: confirmButtonText) + alert.addButton(withTitle: UserText.cancel) + return alert + } + + static func deleteAllPasswordsCompletionAlert(count: Int, syncEnabled: Bool) -> NSAlert { + let messageText = UserText.deleteAllPasswordsCompletionMessageText(count: count) + let informationText = UserText.deleteAllPasswordsCompletionInformationText(syncEnabled: syncEnabled) + return autofillActionCompletionAlert(messageText: messageText, + informationText: informationText) + } + + private static func autofillActionCompletionAlert(messageText: String, informationText: String) -> NSAlert { + let alert = NSAlert() + alert.messageText = messageText + alert.informativeText = informationText + alert.addButton(withTitle: UserText.deleteAllPasswordsCompletionButtonText) + return alert + } } diff --git a/DuckDuckGo/SecureVault/Extensions/NSNotificationName+PasswordManager.swift b/DuckDuckGo/SecureVault/Extensions/NSNotificationName+PasswordManager.swift index 6a95740029..c14f557551 100644 --- a/DuckDuckGo/SecureVault/Extensions/NSNotificationName+PasswordManager.swift +++ b/DuckDuckGo/SecureVault/Extensions/NSNotificationName+PasswordManager.swift @@ -19,7 +19,5 @@ import Foundation extension NSNotification.Name { - static let PasswordManagerChanged = NSNotification.Name("PasswordManagerChanged") - } diff --git a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift index c61f85672a..aefae5c694 100644 --- a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift +++ b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift @@ -126,4 +126,45 @@ extension UserText { static let autoLockThreshold1Hour = NSLocalizedString("pm.lock-screen.threshold.1-hour", value: "1 hour", comment: "Label used when selecting the Auto-Lock threshold") static let autoLockThreshold12Hours = NSLocalizedString("pm.lock-screen.threshold.12-hours", value: "12 hours", comment: "Label used when selecting the Auto-Lock threshold") + // MARK: Autofill Item Deletion (Autofill -> More Menu, Settings -> Autofill) + static let deleteAllPasswords = NSLocalizedString("autofill.items.delete-all-passwords", value: "Delete All Passwords…", comment: "Opens Delete All Passwords dialog") + + // Confirmation Message Text + static func deleteAllPasswordsConfirmationMessageText(count: Int) -> String { + let localized = NSLocalizedString("autofill.items.delete-all-passwords-confirmation-message-text", value: "Are you sure you want to delete all passwords (%d)?", comment: "Message displayed on dialog asking user to confirm deletion of all passwords") + return String(format: localized, count) + } + + // Confirmation Information Text + static func deleteAllPasswordsConfirmationInformationText(syncEnabled: Bool) -> String { + if syncEnabled { + return NSLocalizedString("autofill.items.delete-all-passwords-synced-confirmation-information-text", value: "Your passwords will be deleted from all synced devices. Make sure you still have a way to access your accounts.", comment: "Information message displayed when deleting all passwords on a synced device") + } else { + return NSLocalizedString("autofill.items.delete-all-passwords-device-confirmation-information-text", value: "Your passwords will be deleted from this device. Make sure you still have a way to access your accounts.", comment: "Information message displayed when deleting all passwords on a device") + } + } + + // Completion Message Text + static func deleteAllPasswordsCompletionMessageText(count: Int) -> String { + let localized = NSLocalizedString("autofill.items.delete-all-passwords-completion-message-text", value: "All passwords deleted (%d)", comment: "Message displayed on completion of multiple password deletion") + return String(format: localized, count) + } + + // Completion Information Text + static func deleteAllPasswordsCompletionInformationText(syncEnabled: Bool) -> String { + if syncEnabled { + return NSLocalizedString("autofill.items.delete-all-passwords-synced-completion-information-text", + value: "Your passwords have been deleted from all synced devices.", + comment: "Information message displayed on completion of multiple password deletion when devices are synced") + } else { + return "" + } + } + + // Completion Close Button + static let deleteAllPasswordsCompletionButtonText = NSLocalizedString("autofill.items.delete-all-passwords-completion-button-texy", value: "Close", comment: "Button text on dialog confirming deletion was completed") + + // System Alert Permission Text + static let deleteAllPasswordsPermissionText = NSLocalizedString("autofill.items.delete-all-passwords-permisson-text", value: "delete all passwords", comment: "Message displayed in system authentication dialog") + } diff --git a/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift b/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift index 4f0b2573f2..a87c77ef59 100644 --- a/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift +++ b/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift @@ -250,6 +250,17 @@ final class PasswordManagementItemListModel: ObservableObject { clearSelection() updateFilteredData() + /* + Note: + - The following fixes an long-standing issue where the relevant empty state is not displayed + while switching autofill types when we have no autofill data. + - Not an ideal solution, but acceptable until we better unify how we manage Autofill + state (e.g displayedSections, emptyState) + */ + if emptyState == .noData { + calculateEmptyState() + } + // Select first item if no previous selection was provided if selected == nil { selectFirst() @@ -257,7 +268,7 @@ final class PasswordManagementItemListModel: ObservableObject { } } - @Published private(set) var displayedItems = [PasswordManagementListSection]() { + @Published private(set) var displayedSections = [PasswordManagementListSection]() { didSet { calculateEmptyState() } @@ -315,7 +326,7 @@ final class PasswordManagementItemListModel: ObservableObject { } func select(item: SecureVaultItem, notify: Bool = true) { - for section in displayedItems { + for section in displayedSections { if let first = section.items.first(where: { $0 == item }) { selected(item: first, notify: notify) return @@ -341,7 +352,7 @@ final class PasswordManagementItemListModel: ObservableObject { // If there are no matches for autofill, just pick the first item in the list if let match = bestMatch.first { - for section in displayedItems { + for section in displayedSections { if let account = section.items.first(where: { $0.websiteAccount?.username == match.username && $0.websiteAccount?.domain == match.domain && @@ -361,13 +372,13 @@ final class PasswordManagementItemListModel: ObservableObject { items[index] = item } - var sections = displayedItems + var sections = displayedSections guard let sectionIndex = sections.firstIndex(where: { $0.items.contains(item) }) else { return } - let updatedSection = displayedItems[sectionIndex] + let updatedSection = displayedSections[sectionIndex] var updatedSectionItems = updatedSection.items guard let updatedItemIndex = updatedSectionItems.firstIndex(where: { @@ -377,7 +388,7 @@ final class PasswordManagementItemListModel: ObservableObject { updatedSectionItems[updatedItemIndex] = item sections[sectionIndex] = updatedSection.withUpdatedItems(updatedSectionItems) - displayedItems = sections + displayedSections = sections } func updateFilteredData() { @@ -388,17 +399,17 @@ final class PasswordManagementItemListModel: ObservableObject { itemsByCategory = itemsByCategory.filter { $0.item(matches: filter) } } - if displayedItems.isEmpty && items.isEmpty { + if displayedSections.isEmpty && items.isEmpty { return } switch sortDescriptor.parameter { case .title: - displayedItems = PasswordManagementListSection.sectionsByTLD(with: itemsByCategory, order: sortDescriptor.order) + displayedSections = PasswordManagementListSection.sectionsByTLD(with: itemsByCategory, order: sortDescriptor.order) case .dateCreated: - displayedItems = PasswordManagementListSection.sections(with: itemsByCategory, by: \.created, order: sortDescriptor.order) + displayedSections = PasswordManagementListSection.sections(with: itemsByCategory, by: \.created, order: sortDescriptor.order) case .dateModified: - displayedItems = PasswordManagementListSection.sections(with: itemsByCategory, by: \.lastUpdated, order: sortDescriptor.order) + displayedSections = PasswordManagementListSection.sections(with: itemsByCategory, by: \.lastUpdated, order: sortDescriptor.order) } } @@ -407,7 +418,7 @@ final class PasswordManagementItemListModel: ObservableObject { if passwordManagerCoordinator.isEnabled && (sortDescriptor.category == .allItems || sortDescriptor.category == .logins) { externalPasswordManagerSelected = true - } else if let firstSection = displayedItems.first, let selectedItem = firstSection.items.first { + } else if let firstSection = displayedSections.first, let selectedItem = firstSection.items.first { selected(item: selectedItem) } else { selected(item: nil) @@ -463,7 +474,7 @@ final class PasswordManagementItemListModel: ObservableObject { return } - guard displayedItems.isEmpty else { + guard displayedSections.isEmpty else { emptyState = .none return } diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementItemList.swift b/DuckDuckGo/SecureVault/View/PasswordManagementItemList.swift index 61bd2137a6..92c476fe18 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementItemList.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementItemList.swift @@ -178,7 +178,7 @@ private struct PasswordManagementItemStackContentsView: View { ExternalPasswordManagerItemSection(model: model) } - ForEach(Array(model.displayedItems.enumerated()), id: \.offset) { index, section in + ForEach(Array(model.displayedSections.enumerated()), id: \.offset) { index, section in Section(header: Text(section.title).padding(.leading, 18).padding(.top, index == 0 ? 0 : 10)) { ForEach(section.items, id: \.id) { item in diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift index 602eae318c..4ccb5099e8 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift @@ -49,7 +49,7 @@ final class PasswordManagementViewController: NSViewController { @IBOutlet weak var lockMenuItem: NSMenuItem! @IBOutlet weak var importPasswordMenuItem: NSMenuItem! @IBOutlet weak var settingsMenuItem: NSMenuItem! - + @IBOutlet weak var deleteAllPasswordsMenuItem: NSMenuItem! @IBOutlet weak var unlockYourAutofillLabel: FlatButton! @IBOutlet weak var autofillTitleLabel: NSTextField! @IBOutlet weak var unlockYourAutofillInfo: NSButtonCell! @@ -199,11 +199,12 @@ final class PasswordManagementViewController: NSViewController { private func setupStrings() { importPasswordMenuItem.title = UserText.importPasswords exportLoginItem.title = UserText.exportLogins + deleteAllPasswordsMenuItem.title = UserText.deleteAllPasswords settingsMenuItem.title = UserText.settingsSuspended unlockYourAutofillLabel.title = UserText.passwordManagerUnlockAutofill autofillTitleLabel.stringValue = UserText.autofill - emptyStateTitle.stringValue = UserText.passwordManagerEmptyStateTitle - emptyStateMessage.stringValue = UserText.passwordManagerEmptyStateMessage + emptyStateTitle.stringValue = UserText.pmEmptyStateDefaultTitle + emptyStateMessage.stringValue = UserText.pmEmptyStateDefaultDescription emptyStateButton.title = UserText.importData } @@ -313,6 +314,18 @@ final class PasswordManagementViewController: NSViewController { DataImportView().show() } + @IBAction func onDeleteAllPasswordsClicked(_ sender: Any) { + let builder = AutofillDeleteAllPasswordsBuilder() + guard let autofillDeleteAllPasswordsExecutor = builder.buildExecutor() else { return } + let presenter = builder.buildPresenter() + + presenter.show(actionExecutor: autofillDeleteAllPasswordsExecutor) { + self.refreshData { + self.select(category: .logins) + } + } + } + @IBAction func deviceAuthenticationRequested(_ sender: NSButton) { promptForAuthenticationIfNecessary() } @@ -335,7 +348,7 @@ final class PasswordManagementViewController: NSViewController { self?.searchField.stringValue = text self?.updateFilter() - if clearWhenNoMatches && self?.listModel?.displayedItems.isEmpty == true { + if clearWhenNoMatches && self?.listModel?.displayedSections.isEmpty == true { self?.searchField.stringValue = "" self?.updateFilter() } else if self?.isDirty == false { @@ -697,9 +710,11 @@ final class PasswordManagementViewController: NSViewController { } } - private func refreshData() { + private func refreshData(completion: (() -> Void)? = nil) { self.itemModel?.clearSecureVaultModel() - self.refetchWithText(self.searchField.stringValue) + self.refetchWithText(self.searchField.stringValue) { + completion?() + } self.postChange() } @@ -971,8 +986,8 @@ final class PasswordManagementViewController: NSViewController { private func showEmptyState(category: SecureVaultSorting.Category) { switch category { - case .allItems: showDefaultEmptyState() - case .logins: showEmptyState(image: .loginsEmpty, title: UserText.pmEmptyStateLoginsTitle) + case .allItems: showEmptyState(image: .loginsEmpty, title: UserText.pmEmptyStateDefaultTitle, message: UserText.pmEmptyStateDefaultDescription, hideMessage: false, hideButton: false) + case .logins: showEmptyState(image: .loginsEmpty, title: UserText.pmEmptyStateLoginsTitle, hideMessage: false, hideButton: false) case .identities: showEmptyState(image: .identitiesEmpty, title: UserText.pmEmptyStateIdentitiesTitle) case .cards: showEmptyState(image: .creditCardsEmpty, title: UserText.pmEmptyStateCardsTitle) } @@ -982,24 +997,15 @@ final class PasswordManagementViewController: NSViewController { emptyState.isHidden = true } - private func showDefaultEmptyState() { - emptyState.isHidden = false - emptyStateMessage.isHidden = false - emptyStateButton.isHidden = false - - emptyStateImageView.image = .loginsEmpty - - emptyStateTitle.attributedStringValue = NSAttributedString.make(UserText.pmEmptyStateDefaultTitle, lineHeight: 1.14, kern: -0.23) - emptyStateMessage.attributedStringValue = NSAttributedString.make(UserText.pmEmptyStateDefaultDescription, lineHeight: 1.05, kern: -0.08) - } - - private func showEmptyState(image: NSImage, title: String) { + private func showEmptyState(image: NSImage, title: String, message: String? = nil, hideMessage: Bool = true, hideButton: Bool = true) { emptyState.isHidden = false - emptyStateImageView.image = image emptyStateTitle.attributedStringValue = NSAttributedString.make(title, lineHeight: 1.14, kern: -0.23) - emptyStateMessage.isHidden = true - emptyStateButton.isHidden = true + if let message { + emptyStateMessage.attributedStringValue = NSAttributedString.make(message, lineHeight: 1.05, kern: -0.08) + } + emptyStateMessage.isHidden = hideMessage + emptyStateButton.isHidden = hideButton } private func requestSync() { @@ -1054,3 +1060,22 @@ extension PasswordManagementViewController: NSTextViewDelegate { } } + +extension PasswordManagementViewController: NSMenuItemValidation { + + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { + switch menuItem.action { + case #selector(PasswordManagementViewController.onDeleteAllPasswordsClicked(_:)): + return haveDuckDuckGoPasswords + default: + guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return false } + return appDelegate.validateMenuItem(menuItem) + } + } + + private var haveDuckDuckGoPasswords: Bool { + guard let vault = try? AutofillSecureVaultFactory.makeVault(errorReporter: SecureVaultErrorReporter.shared) else { return false } + let accounts = (try? vault.accounts()) ?? [] + return !accounts.isEmpty + } +} diff --git a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard index efa488a53a..9c71246fa6 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard +++ b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard @@ -73,7 +73,7 @@ - + @@ -347,6 +347,7 @@ + @@ -390,6 +391,13 @@ + + + + + + + diff --git a/UnitTests/Autofill/Mocks/MockAutofillActionExecutor.swift b/UnitTests/Autofill/Mocks/MockAutofillActionExecutor.swift new file mode 100644 index 0000000000..bb7a4ec8bb --- /dev/null +++ b/UnitTests/Autofill/Mocks/MockAutofillActionExecutor.swift @@ -0,0 +1,61 @@ +// +// MockAutofillActionExecutor.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import BrowserServicesKit +import DDGSync +import Foundation +@testable import DuckDuckGo_Privacy_Browser + +final class MockAutofillActionBuilder: AutofillActionBuilder { + + var mockExecutor: MockAutofillActionExecutor? + var mockPresenter: MockAutofillActionPresenter? + + func buildExecutor() -> AutofillActionExecutor? { + guard let secureVault = try? MockSecureVaultFactory.makeVault(errorReporter: nil) else { return nil } + let syncService = MockDDGSyncing(authState: .inactive, scheduler: CapturingScheduler(), isSyncInProgress: false) + let executor = MockAutofillActionExecutor(userAuthenticator: UserAuthenticatorMock(), secureVault: secureVault, syncService: syncService) + self.mockExecutor = executor + return executor + } + + func buildPresenter() -> AutofillActionPresenter { + let presenter = MockAutofillActionPresenter() + self.mockPresenter = presenter + return presenter + } +} + +final class MockAutofillActionExecutor: AutofillActionExecutor { + + var didExecute = false + + init(userAuthenticator: UserAuthenticating, secureVault: any AutofillSecureVault, syncService: DDGSyncing) { } + + var confirmationAlert: NSAlert { + NSAlert() + } + + var completionAlert: NSAlert { + NSAlert() + } + + func execute(_ onSuccess: (() -> Void)?) { + didExecute = true + } +} diff --git a/UnitTests/Autofill/Mocks/MockAutofillActionPresenter.swift b/UnitTests/Autofill/Mocks/MockAutofillActionPresenter.swift new file mode 100644 index 0000000000..cc1d3264aa --- /dev/null +++ b/UnitTests/Autofill/Mocks/MockAutofillActionPresenter.swift @@ -0,0 +1,30 @@ +// +// MockAutofillActionPresenter.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import DuckDuckGo_Privacy_Browser + +final class MockAutofillActionPresenter: AutofillActionPresenter { + + var didCallShow = false + + func show(actionExecutor: AutofillActionExecutor, completion: () -> Void) { + didCallShow = true + actionExecutor.execute(nil) + } +} diff --git a/UnitTests/Autofill/Tests/AutofillDeleteAllPasswordsExecutorTests.swift b/UnitTests/Autofill/Tests/AutofillDeleteAllPasswordsExecutorTests.swift new file mode 100644 index 0000000000..f34960a14f --- /dev/null +++ b/UnitTests/Autofill/Tests/AutofillDeleteAllPasswordsExecutorTests.swift @@ -0,0 +1,83 @@ +// +// AutofillDeleteAllPasswordsExecutorTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import BrowserServicesKit +import DDGSync +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class AutofillDeleteAllPasswordsExecutorTests: XCTestCase { + + private var sut: AutofillDeleteAllPasswordsExecutor! + private let mockAuthenticator = UserAuthenticatorMock() + private var secureVault: MockSecureVault! + private let scheduler = CapturingScheduler() + private var syncService: DDGSyncing! + + override func setUpWithError() throws { + secureVault = try MockSecureVaultFactory.makeVault(errorReporter: nil) + syncService = MockDDGSyncing(authState: .inactive, scheduler: scheduler, isSyncInProgress: false) + sut = .init(userAuthenticator: mockAuthenticator, secureVault: secureVault, syncService: syncService) + } + + func testExecuteCallsAuthenticate() async throws { + // Given + let expectation = expectation(description: "called authenticate") + XCTAssertFalse(mockAuthenticator.didCallAuthenticate) + + // When + sut.execute { + expectation.fulfill() + } + + // Then + await fulfillment(of: [expectation], timeout: 3) + XCTAssertTrue(mockAuthenticator.didCallAuthenticate) + } + + func testExecuteCallsDeletesAllFromVault() async throws { + // Given + let expectation = expectation(description: "called delete all from vault") + secureVault.addWebsiteCredentials(identifiers: [1]) + XCTAssert(secureVault.storedCredentials.count == 1) + + // When + sut.execute { + expectation.fulfill() + } + + // Then + await fulfillment(of: [expectation], timeout: 3) + XCTAssert(secureVault.storedCredentials.isEmpty) + } + + func testExecuteCallsNotifyDataChanged() async throws { + // Given + let expectation = expectation(description: "called sync immediately") + XCTAssertFalse(scheduler.notifyDataChangedCalled) + + // When + sut.execute { + expectation.fulfill() + } + + // Then + await fulfillment(of: [expectation], timeout: 3) + XCTAssertTrue(scheduler.notifyDataChangedCalled) + } +} diff --git a/UnitTests/DataExport/CSVLoginExporterTests.swift b/UnitTests/DataExport/CSVLoginExporterTests.swift index 7a4576a48d..20d244968e 100644 --- a/UnitTests/DataExport/CSVLoginExporterTests.swift +++ b/UnitTests/DataExport/CSVLoginExporterTests.swift @@ -27,9 +27,7 @@ class CSVLoginExporterTests: XCTestCase { let mockFileStore = FileStoreMock() let vault = try MockSecureVaultFactory.makeVault(errorReporter: nil) - let credentials = websiteCredentials(identifiers: [1]) - vault.storedAccounts = credentials.map(\.value.account) - vault.storedCredentials = credentials + vault.addWebsiteCredentials(identifiers: [1]) let exporter = CSVLoginExporter(secureVault: vault, fileStore: mockFileStore) @@ -43,20 +41,4 @@ class CSVLoginExporterTests: XCTestCase { let expectedRow = "\"title-1\",\"domain-1\",\"user-1\",\"password\\\"containing\\\"quotes\"" XCTAssertEqual(data, (expectedHeader + expectedRow).data(using: .utf8)!) } - - private func websiteCredentials(identifiers: [Int64]) -> [Int64: SecureVaultModels.WebsiteCredentials] { - var credentials = [Int64: SecureVaultModels.WebsiteCredentials]() - - for identifier in identifiers { - let account = SecureVaultModels.WebsiteAccount(id: String(identifier), - title: "title-\(identifier)", - username: "user-\(identifier)", - domain: "domain-\(identifier)") - let credential = SecureVaultModels.WebsiteCredentials(account: account, password: "password\"containing\"quotes".data(using: .utf8)!) - credentials[identifier] = credential - } - - return credentials - } - } diff --git a/UnitTests/DataExport/MockSecureVault.swift b/UnitTests/DataExport/MockSecureVault.swift index cdc4f5a2b2..8e30d0973b 100644 --- a/UnitTests/DataExport/MockSecureVault.swift +++ b/UnitTests/DataExport/MockSecureVault.swift @@ -228,6 +228,24 @@ final class MockSecureVault: AutofillSecureVault { } +extension MockSecureVault { + func addWebsiteCredentials(identifiers: [Int64]) { + var credentials = [Int64: SecureVaultModels.WebsiteCredentials]() + + for identifier in identifiers { + let account = SecureVaultModels.WebsiteAccount(id: String(identifier), + title: "title-\(identifier)", + username: "user-\(identifier)", + domain: "domain-\(identifier)") + let credential = SecureVaultModels.WebsiteCredentials(account: account, password: "password\"containing\"quotes".data(using: .utf8)!) + credentials[identifier] = credential + } + + self.storedAccounts = credentials.map(\.value.account) + self.storedCredentials = credentials + } +} + // MARK: - Mock Providers private extension URL { diff --git a/UnitTests/Preferences/AutofillPreferencesModelTests.swift b/UnitTests/Preferences/AutofillPreferencesModelTests.swift index 6c77002c64..044a56b252 100644 --- a/UnitTests/Preferences/AutofillPreferencesModelTests.swift +++ b/UnitTests/Preferences/AutofillPreferencesModelTests.swift @@ -32,14 +32,17 @@ final class AutofillPreferencesPersistorMock: AutofillPreferencesPersistor { } final class UserAuthenticatorMock: UserAuthenticating { + var didCallAuthenticate = false var _authenticateUser: (DeviceAuthenticator.AuthenticationReason) -> DeviceAuthenticationResult = { _ in return .success } func authenticateUser(reason: DeviceAuthenticator.AuthenticationReason, result: @escaping (DeviceAuthenticationResult) -> Void) { + didCallAuthenticate = true let authenticationResult = _authenticateUser(reason) result(authenticationResult) } func authenticateUser(reason: DeviceAuthenticator.AuthenticationReason) async -> DeviceAuthenticationResult { - _authenticateUser(reason) + didCallAuthenticate = true + return _authenticateUser(reason) } } diff --git a/UnitTests/SecureVault/PasswordManagementItemListModelTests.swift b/UnitTests/SecureVault/PasswordManagementItemListModelTests.swift index 553b1ad84d..cbd6b6c569 100644 --- a/UnitTests/SecureVault/PasswordManagementItemListModelTests.swift +++ b/UnitTests/SecureVault/PasswordManagementItemListModelTests.swift @@ -52,14 +52,14 @@ final class PasswordManagementItemListModelTests: XCTestCase { model.update(items: createdAccounts) model.filter = "domain5" - XCTAssertEqual(model.displayedItems.count, 1) + XCTAssertEqual(model.displayedSections.count, 1) - let filteredAccounts = accounts(from: model.displayedItems) + let filteredAccounts = accounts(from: model.displayedSections) XCTAssertEqual(filteredAccounts[0].domain, "domain5") model.filter = "" - let unfilteredAccounts = accounts(from: model.displayedItems) + let unfilteredAccounts = accounts(from: model.displayedSections) XCTAssertEqual(unfilteredAccounts.count, 10) XCTAssertEqual(unfilteredAccounts[0].domain, "domain0") XCTAssertEqual(unfilteredAccounts[9].domain, "domain9")