diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index b6f88103f6..ba0dd6986e 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 291 +CURRENT_PROJECT_VERSION = 296 diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3a617e57b3..a1e752a328 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -54,8 +54,6 @@ 1D01A3D92B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01A3D72B88DF8B00FE8150 /* PreferencesSyncView.swift */; }; 1D02633628D8A9A9005CBB41 /* BWEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D02633528D8A9A9005CBB41 /* BWEncryption.m */; settings = {COMPILER_FLAGS = "-Wno-deprecated -Wno-strict-prototypes"; }; }; 1D074B272909A433006E4AC3 /* PasswordManagerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D074B262909A433006E4AC3 /* PasswordManagerCoordinator.swift */; }; - 1D0DE93E2C3BA9840037ABC2 /* AppRestarter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0DE93D2C3BA9840037ABC2 /* AppRestarter.swift */; }; - 1D0DE93F2C3BA9840037ABC2 /* AppRestarter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0DE93D2C3BA9840037ABC2 /* AppRestarter.swift */; }; 1D0DE9412C3BB9CC0037ABC2 /* ReleaseNotesParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0DE9402C3BB9CC0037ABC2 /* ReleaseNotesParser.swift */; }; 1D0DE9422C3BB9CC0037ABC2 /* ReleaseNotesParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0DE9402C3BB9CC0037ABC2 /* ReleaseNotesParser.swift */; }; 1D12F2E2298BC660009A65FD /* InternalUserDeciderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */; }; @@ -69,8 +67,6 @@ 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BF72B86192200F8BBC6 /* PreferencesEmailProtectionView.swift */; }; 1D220BFC2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */; }; 1D220BFD2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */; }; - 1D232E942C7860DA0043840D /* BinaryOwnershipChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D232E932C7860DA0043840D /* BinaryOwnershipChecker.swift */; }; - 1D232E992C7870D90043840D /* BinaryOwnershipCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D232E962C786E7D0043840D /* BinaryOwnershipCheckerTests.swift */; }; 1D26EBAC2B74BECB0002A93F /* NSImageSendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */; }; 1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */; }; 1D26EBB02B74DB600002A93F /* TabSnapshotCleanupService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D26EBAF2B74DB600002A93F /* TabSnapshotCleanupService.swift */; }; @@ -1844,6 +1840,8 @@ 843965132C6F2FFE004C8899 /* NSDragOperationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843965112C6F2FFE004C8899 /* NSDragOperationExtension.swift */; }; 843965152C737022004C8899 /* NSPasteboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843965142C737022004C8899 /* NSPasteboardExtension.swift */; }; 843965162C737022004C8899 /* NSPasteboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843965142C737022004C8899 /* NSPasteboardExtension.swift */; }; + 843AD3DC2CD389CC00163067 /* XMLNodeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843AD3DB2CD389C500163067 /* XMLNodeExtension.swift */; }; + 843AD3DD2CD389CC00163067 /* XMLNodeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843AD3DB2CD389C500163067 /* XMLNodeExtension.swift */; }; 843D73BB2C786E5400E4F9DC /* BookmarkListPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F1C8DA2C774CA900716446 /* BookmarkListPopover.swift */; }; 843D73BC2C786E5400E4F9DC /* BookmarkListPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F1C8DA2C774CA900716446 /* BookmarkListPopover.swift */; }; 844D7DA42C9443EA00BE61D4 /* NSPrintInfoExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844D7DA32C9443E500BE61D4 /* NSPrintInfoExtension.swift */; }; @@ -1934,6 +1932,8 @@ 85AC3B4925DAC9BD00C7D2AA /* ConfigurationStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC3B4825DAC9BD00C7D2AA /* ConfigurationStorageTests.swift */; }; 85AC7ADB27BD628400FFB69B /* HomePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC7ADA27BD628400FFB69B /* HomePage.swift */; }; 85AC7ADD27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC7ADC27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift */; }; + 85B49AFA2CD1B7C5007FAA2A /* SystemInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B49AF92CD1B7C5007FAA2A /* SystemInfo.swift */; }; + 85B49AFB2CD1B7C5007FAA2A /* SystemInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B49AF92CD1B7C5007FAA2A /* SystemInfo.swift */; }; 85B7184A27677C2D00B4277F /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85B7184927677C2D00B4277F /* Onboarding.storyboard */; }; 85B7184C27677C6500B4277F /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B7184B27677C6500B4277F /* OnboardingViewController.swift */; }; 85B7184E27677CBB00B4277F /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B7184D27677CBB00B4277F /* RootView.swift */; }; @@ -2762,6 +2762,7 @@ BD384ACA2BBC821A00EF3735 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; BD384ACB2BBC821B00EF3735 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; BD384ACC2BBC821B00EF3735 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; + BD6367252C877BE1009DE7A8 /* UpdateUserDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD6367242C877BE1009DE7A8 /* UpdateUserDriver.swift */; }; BD7090CF2C5182FB009EED82 /* UnifiedFeedbackFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7090CE2C5182FB009EED82 /* UnifiedFeedbackFormView.swift */; }; BD7090D02C5182FB009EED82 /* UnifiedFeedbackFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7090CE2C5182FB009EED82 /* UnifiedFeedbackFormView.swift */; }; BD7090D22C52ECFE009EED82 /* UnifiedMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7090D12C52ECFE009EED82 /* UnifiedMetadataCollector.swift */; }; @@ -2984,6 +2985,8 @@ EED9A6762C37FE6900E0FAB9 /* login_deduplication_test_data.csv in Resources */ = {isa = PBXBuildFile; fileRef = EED9A6732C37FE6800E0FAB9 /* login_deduplication_test_data.csv */; }; EED9A6772C37FE6900E0FAB9 /* login_deduplication_starting_data.csv in Resources */ = {isa = PBXBuildFile; fileRef = EED9A6742C37FE6900E0FAB9 /* login_deduplication_starting_data.csv */; }; EED9A6782C37FE6900E0FAB9 /* login_deduplication_starting_data.csv in Resources */ = {isa = PBXBuildFile; fileRef = EED9A6742C37FE6900E0FAB9 /* login_deduplication_starting_data.csv */; }; + EEDFA38A2CD148DE00D1C558 /* SyncDiagnosisHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDFA3892CD148DE00D1C558 /* SyncDiagnosisHelper.swift */; }; + EEDFA38B2CD148DE00D1C558 /* SyncDiagnosisHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDFA3892CD148DE00D1C558 /* SyncDiagnosisHelper.swift */; }; EEE0E1CD2C32F5690058E148 /* CSVImporterIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE0E1CC2C32F5690058E148 /* CSVImporterIntegrationTests.swift */; }; EEE0E1CF2C32F6530058E148 /* mock_login_data_large.csv in Resources */ = {isa = PBXBuildFile; fileRef = EEE0E1CE2C32F6530058E148 /* mock_login_data_large.csv */; }; EEE0E1D02C32F6530058E148 /* mock_login_data_large.csv in Resources */ = {isa = PBXBuildFile; fileRef = EEE0E1CE2C32F6530058E148 /* mock_login_data_large.csv */; }; @@ -3296,7 +3299,6 @@ 1D02633428D8A9A9005CBB41 /* BWEncryption.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BWEncryption.h; sourceTree = ""; }; 1D02633528D8A9A9005CBB41 /* BWEncryption.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BWEncryption.m; sourceTree = ""; }; 1D074B262909A433006E4AC3 /* PasswordManagerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordManagerCoordinator.swift; sourceTree = ""; }; - 1D0DE93D2C3BA9840037ABC2 /* AppRestarter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRestarter.swift; sourceTree = ""; }; 1D0DE9402C3BB9CC0037ABC2 /* ReleaseNotesParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseNotesParser.swift; sourceTree = ""; }; 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalUserDeciderStoreMock.swift; sourceTree = ""; }; 1D1A33482A6FEB170080ACED /* BurnerMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BurnerMode.swift; sourceTree = ""; }; @@ -3304,8 +3306,6 @@ 1D1C36E529FB019C001FA40C /* HistoryTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTabExtensionTests.swift; sourceTree = ""; }; 1D220BF72B86192200F8BBC6 /* PreferencesEmailProtectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesEmailProtectionView.swift; sourceTree = ""; }; 1D220BFB2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyProtectionStatus.swift; sourceTree = ""; }; - 1D232E932C7860DA0043840D /* BinaryOwnershipChecker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinaryOwnershipChecker.swift; sourceTree = ""; }; - 1D232E962C786E7D0043840D /* BinaryOwnershipCheckerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinaryOwnershipCheckerTests.swift; sourceTree = ""; }; 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSImageSendable.swift; sourceTree = ""; }; 1D26EBAF2B74DB600002A93F /* TabSnapshotCleanupService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSnapshotCleanupService.swift; sourceTree = ""; }; 1D36E657298AA3BA00AA485D /* InternalUserDeciderStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalUserDeciderStore.swift; sourceTree = ""; }; @@ -3995,6 +3995,7 @@ 8426108C2C9811EC0070D5F9 /* KeyEquivalentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyEquivalentView.swift; sourceTree = ""; }; 843965112C6F2FFE004C8899 /* NSDragOperationExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDragOperationExtension.swift; sourceTree = ""; }; 843965142C737022004C8899 /* NSPasteboardExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPasteboardExtension.swift; sourceTree = ""; }; + 843AD3DB2CD389C500163067 /* XMLNodeExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLNodeExtension.swift; sourceTree = ""; }; 844D7DA32C9443E500BE61D4 /* NSPrintInfoExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPrintInfoExtension.swift; sourceTree = ""; }; 84537A022C998C24008723BC /* FireWindowSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireWindowSession.swift; sourceTree = ""; }; 848648A02C76F4B20082282D /* BookmarksBarMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarMenuViewController.swift; sourceTree = ""; }; @@ -4067,6 +4068,7 @@ 85AC7ADA27BD628400FFB69B /* HomePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePage.swift; sourceTree = ""; }; 85AC7ADC27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDefaultBrowserModel.swift; sourceTree = ""; }; 85AE2FF124A33A2D002D507F /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + 85B49AF92CD1B7C5007FAA2A /* SystemInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemInfo.swift; sourceTree = ""; }; 85B7184927677C2D00B4277F /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = ""; }; 85B7184B27677C6500B4277F /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; 85B7184D27677CBB00B4277F /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; @@ -4657,6 +4659,7 @@ BBFF355C2C4AF26200DA3289 /* BookmarksSortModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksSortModeTests.swift; sourceTree = ""; }; BD384AC72BBC821100EF3735 /* vpn-light-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-light-mode.json"; sourceTree = ""; }; BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-dark-mode.json"; sourceTree = ""; }; + BD6367242C877BE1009DE7A8 /* UpdateUserDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateUserDriver.swift; sourceTree = ""; }; BD7090CE2C5182FB009EED82 /* UnifiedFeedbackFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedFeedbackFormView.swift; sourceTree = ""; }; BD7090D12C52ECFE009EED82 /* UnifiedMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedMetadataCollector.swift; sourceTree = ""; }; BD7090D52C540D5D009EED82 /* EmptyMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyMetadataCollector.swift; sourceTree = ""; }; @@ -4760,6 +4763,7 @@ EED735352BB46B6000F173D6 /* AutocompleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteTests.swift; sourceTree = ""; }; EED9A6732C37FE6800E0FAB9 /* login_deduplication_test_data.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = login_deduplication_test_data.csv; sourceTree = ""; }; EED9A6742C37FE6900E0FAB9 /* login_deduplication_starting_data.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = login_deduplication_starting_data.csv; sourceTree = ""; }; + EEDFA3892CD148DE00D1C558 /* SyncDiagnosisHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDiagnosisHelper.swift; sourceTree = ""; }; EEE0E1CC2C32F5690058E148 /* CSVImporterIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVImporterIntegrationTests.swift; sourceTree = ""; }; EEE0E1CE2C32F6530058E148 /* mock_login_data_large.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = mock_login_data_large.csv; sourceTree = ""; }; EEE11C5D2C7F54AD000ABD7E /* AutofillLoginImportState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginImportState.swift; sourceTree = ""; }; @@ -5229,12 +5233,11 @@ 1DA84D312C119AE70011C80F /* UpdateMenuItemFactory.swift */, 1D72D59B2BFF61B200AEDE36 /* UpdateNotificationPresenter.swift */, 1D9297BE2C1B062900A38521 /* ApplicationUpdateDetector.swift */, - 1D0DE93D2C3BA9840037ABC2 /* AppRestarter.swift */, - 1D232E932C7860DA0043840D /* BinaryOwnershipChecker.swift */, 1D39E5762C2BFD5700757339 /* ReleaseNotesTabExtension.swift */, 1D39E5792C2C0F3700757339 /* ReleaseNotesUserScript.swift */, 1D0DE9402C3BB9CC0037ABC2 /* ReleaseNotesParser.swift */, 1D710F4A2C48F1F200C3975F /* UpdateDialogHelper.swift */, + BD6367242C877BE1009DE7A8 /* UpdateUserDriver.swift */, ); path = Updates; sourceTree = ""; @@ -5271,7 +5274,6 @@ children = ( 1D838A312C44F0180078373F /* ReleaseNotesParserTests.swift */, 1D638D602C44F2BA00530DD5 /* ApplicationUpdateDetectorTests.swift */, - 1D232E962C786E7D0043840D /* BinaryOwnershipCheckerTests.swift */, ); path = Updates; sourceTree = ""; @@ -5572,6 +5574,7 @@ 37CEFCA82A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift */, 37A6A8F02AFCC988008580A3 /* FaviconsFetcherOnboarding.swift */, 37A6A8F52AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift */, + EEDFA3892CD148DE00D1C558 /* SyncDiagnosisHelper.swift */, ); path = Sync; sourceTree = ""; @@ -8084,6 +8087,7 @@ B634DBE2293C8FFF00C3C99E /* UserDialogRequest.swift */, 1D1A33482A6FEB170080ACED /* BurnerMode.swift */, 56406D4A2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift */, + 85B49AF92CD1B7C5007FAA2A /* SystemInfo.swift */, ); path = Model; sourceTree = ""; @@ -8501,6 +8505,7 @@ AA6FFB4324DC33320028F4D0 /* NSViewExtension.swift */, AA9E9A5525A3AE8400D1959D /* NSWindowExtension.swift */, B643BF1327ABF772000BACEC /* NSWorkspaceExtension.swift */, + 843AD3DB2CD389C500163067 /* XMLNodeExtension.swift */, B6A9E46A2614618A0067D1B9 /* OperatingSystemVersionExtension.swift */, EEE50C282C38249C003DD7FF /* OptionalExtension.swift */, B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */, @@ -10935,6 +10940,7 @@ 3706FAD4293F65D500E42796 /* DataExtension.swift in Sources */, 3706FAD6293F65D500E42796 /* ConfigurationStore.swift in Sources */, FD22255E2C64B68500199373 /* AutoconsentExperiment.swift in Sources */, + EEDFA38B2CD148DE00D1C558 /* SyncDiagnosisHelper.swift in Sources */, 3706FAD7293F65D500E42796 /* Feedback.swift in Sources */, 1D0DE9422C3BB9CC0037ABC2 /* ReleaseNotesParser.swift in Sources */, EED4D3D92C874AE200C79EEA /* PopoverInfoViewController.swift in Sources */, @@ -11318,6 +11324,7 @@ 3706FBB5293F65D500E42796 /* UserContentUpdating.swift in Sources */, 4B4D60B72A0C847D00BCD287 /* NetworkProtectionNavBarButtonModel.swift in Sources */, 3706FBB6293F65D500E42796 /* ChromiumPreferences.swift in Sources */, + 85B49AFB2CD1B7C5007FAA2A /* SystemInfo.swift in Sources */, 3706FBB7293F65D500E42796 /* FirePopoverViewController.swift in Sources */, 3706FBB8293F65D500E42796 /* SavePaymentMethodPopover.swift in Sources */, B6CC266D2BAD9CD800F53F8D /* FileProgressPresenter.swift in Sources */, @@ -11530,6 +11537,7 @@ C18194602C7CDD0E00381092 /* PromotionViewModel.swift in Sources */, 3706FC31293F65D500E42796 /* PermissionButton.swift in Sources */, 9F6434622BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */, + 843AD3DC2CD389CC00163067 /* XMLNodeExtension.swift in Sources */, 3706FC32293F65D500E42796 /* MoreOptionsMenu.swift in Sources */, 3706FC34293F65D500E42796 /* PermissionAuthorizationViewController.swift in Sources */, 3706FC35293F65D500E42796 /* BookmarkNode.swift in Sources */, @@ -11714,7 +11722,6 @@ 3707C71E294B5D2900682A9F /* URLRequestExtension.swift in Sources */, 3706FCA3293F65D500E42796 /* WKProcessPool+GeolocationProvider.swift in Sources */, 372A0FED2B2379310033BF7F /* SyncMetricsEventsHandler.swift in Sources */, - 1D0DE93F2C3BA9840037ABC2 /* AppRestarter.swift in Sources */, 3706FCA4293F65D500E42796 /* RecentlyClosedMenu.swift in Sources */, 8400DC4C2C6E26AE006509D2 /* ItemCachingCollectionView.swift in Sources */, 4B9DB02D2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */, @@ -12636,7 +12643,6 @@ B6BF5D8929470BC4006742B1 /* HTTPSUpgradeTabExtension.swift in Sources */, 1D36E65B298ACD2900AA485D /* AppIconChanger.swift in Sources */, 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */, - 1D0DE93E2C3BA9840037ABC2 /* AppRestarter.swift in Sources */, 7BCB90C22C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift in Sources */, 4B9DB0202A983B24000927DB /* ProductWaitlistRequest.swift in Sources */, 7B60B0022C5145EC008E32A3 /* VPNUIActionHandler.swift in Sources */, @@ -12775,6 +12781,7 @@ 3797C7A02C61806500DA77FB /* HomePageSettingsView.swift in Sources */, B6F41031264D2B23003DA42C /* ProgressExtension.swift in Sources */, 4B723E0F26B0006500E14D75 /* CSVParser.swift in Sources */, + 843AD3DD2CD389CC00163067 /* XMLNodeExtension.swift in Sources */, B63BDF7E27FDAA640072D75B /* PrivacyDashboardWebView.swift in Sources */, 37CD54CF27F2FDD100F1F7B9 /* AppearancePreferences.swift in Sources */, 3199AF7B2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift in Sources */, @@ -12883,6 +12890,7 @@ 3706FEC8293F6F7500E42796 /* BWManagement.swift in Sources */, 316C48F02CC2B232000B08C1 /* AIChatPreferencesStorage.swift in Sources */, B6830961274CDE99004B46BB /* FireproofDomainsContainer.swift in Sources */, + 85B49AFA2CD1B7C5007FAA2A /* SystemInfo.swift in Sources */, B687B7CC2947A1E9001DEA6F /* ExternalAppSchemeHandler.swift in Sources */, B65536AE2685E17200085A79 /* GeolocationService.swift in Sources */, 4B02198925E05FAC00ED7DEA /* FireproofingURLExtensions.swift in Sources */, @@ -12903,6 +12911,7 @@ 856CADF0271710F400E79BB0 /* HoverUserScript.swift in Sources */, B6DE57F62B05EA9000CD54B9 /* SheetHostingWindow.swift in Sources */, AA6EF9B525081B4C004754E6 /* MainMenuActions.swift in Sources */, + EEDFA38A2CD148DE00D1C558 /* SyncDiagnosisHelper.swift in Sources */, EED4D3D82C874AE200C79EEA /* PopoverInfoViewController.swift in Sources */, 56A0541F2C1CA1F5007D8FAB /* OnboardingTabExtension.swift in Sources */, B63D466925BEB6C200874977 /* WKWebView+SessionState.swift in Sources */, @@ -13156,7 +13165,6 @@ 37BF3F21286F0A7A00BD9014 /* PinnedTabsViewModel.swift in Sources */, EEC4A6692B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, AAC5E4D225D6A709007F5990 /* BookmarkList.swift in Sources */, - 1D232E942C7860DA0043840D /* BinaryOwnershipChecker.swift in Sources */, B602E81D2A1E25B1006D261F /* NEOnDemandRuleExtension.swift in Sources */, 56A0543E2C215FB3007D8FAB /* OnboardingUserScript.swift in Sources */, C1372EF42BBC5BAD003F8793 /* SecureTextField.swift in Sources */, @@ -13185,6 +13193,7 @@ 37219B3A2CBFD4F300C9D7A8 /* NewTabPageSearchBoxExperiment+Logger.swift in Sources */, 37AAA41C2C9CB9C0002A5377 /* AddressBarTextFieldView.swift in Sources */, AA6820EB25503D6A005ED0D5 /* Fire.swift in Sources */, + BD6367252C877BE1009DE7A8 /* UpdateUserDriver.swift in Sources */, 3158B1492B0BF73000AF130C /* DBPHomeViewController.swift in Sources */, 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 37445F9C2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */, @@ -13504,7 +13513,6 @@ 567A23E12C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift in Sources */, 986189E62A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift in Sources */, AA652CD325DDA6E9009059CC /* LocalBookmarkManagerTests.swift in Sources */, - 1D232E992C7870D90043840D /* BinaryOwnershipCheckerTests.swift in Sources */, CBDD5DE329A67F2700832877 /* MockConfigurationStore.swift in Sources */, 9F3910692B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */, 560C6ED02CCA5C6000D411E2 /* CapturingOnboardingNavigationDelegate.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 189e309997..69e9269485 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { "branch" : "sabrina/onboarding-import-state", - "revision" : "05ea8316abb7cf3ba312b749b09ea2a437aa188a" + "revision" : "3aa45c028914206c3ecc24fcd51d365af32ca8fa" } }, { @@ -42,7 +42,7 @@ "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { "branch" : "pr-releases/pr-1180", - "revision" : "551d15fcadecd1e8cddd0dca625358e8bf1fb3fa" + "revision" : "82be5d3ada8ff7743b08e5139c9e9ba83d66841c" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "9de2b2aa317a48d3ee31116dc15b0feeb2cc9414", - "version" : "5.3.0" + "revision" : "53fd1a0f8d91fcf475d9220f810141007300dffd", + "version" : "7.1.1" } }, { @@ -167,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/sync_crypto", "state" : { - "revision" : "2ab6ab6f0f96b259c14c2de3fc948935fc16ac78", - "version" : "0.2.0" + "revision" : "0c8bf3c0e75591bc366407b9d7a73a9fcfc7736f", + "version" : "0.3.0" } }, { diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 2eb37124d8..8bce0390f3 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -97,7 +97,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { public let subscriptionManager: SubscriptionManager public let subscriptionUIHandler: SubscriptionUIHandling - public let subscriptionCookieManager: SubscriptionCookieManaging + private let subscriptionCookieManager: SubscriptionCookieManaging + private var subscriptionCookieManagerFeatureFlagCancellable: AnyCancellable? // MARK: - Freemium DBP public let freemiumDBPFeature: FreemiumDBPFeature @@ -325,6 +326,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { freemiumDBPFeature.subscribeToDependencyUpdates() } + // swiftlint:disable:next cyclomatic_complexity func applicationDidFinishLaunching(_ notification: Notification) { guard NSApp.runType.requiresEnvironment else { return } defer { @@ -367,6 +369,30 @@ final class AppDelegate: NSObject, NSApplicationDelegate { subscriptionManager.loadInitialData() + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + + // Enable subscriptionCookieManager if feature flag is present + if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) { + subscriptionCookieManager.enableSettingSubscriptionCookie() + } + + // Keep track of feature flag changes + subscriptionCookieManagerFeatureFlagCancellable = privacyConfigurationManager.updatesPublisher + .sink { [weak self, weak privacyConfigurationManager] in + guard let self, let privacyConfigurationManager else { return } + + let isEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) + + Task { [weak self] in + if isEnabled { + self?.subscriptionCookieManager.enableSettingSubscriptionCookie() + await self?.subscriptionCookieManager.refreshSubscriptionCookie() + } else { + await self?.subscriptionCookieManager.disableSettingSubscriptionCookie() + } + } + } + if [.normal, .uiTests].contains(NSApp.runType) { stateRestorationManager.applicationDidFinishLaunching() } @@ -465,8 +491,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { guard didFinishLaunching else { return } PixelExperiment.fireOnboardingTestPixels() - syncService?.initializeIfNeeded() - syncService?.scheduler.notifyAppLifecycleEvent() + initializeSync() NetworkProtectionAppEvents(featureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager)).applicationDidBecomeActive() @@ -492,6 +517,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } } + private func initializeSync() { + guard let syncService else { return } + syncService.initializeIfNeeded() + syncService.scheduler.notifyAppLifecycleEvent() + SyncDiagnosisHelper(syncService: syncService).diagnoseAccountStatus() + } + func applicationDidResignActive(_ notification: Notification) { Task { @MainActor in await vpnRedditSessionWorkaround.removeRedditSessionWorkaround() diff --git a/DuckDuckGo/Assets.xcassets/Images/Check.imageset/Check-Color-16.svg b/DuckDuckGo/Assets.xcassets/Images/Check.imageset/Check-Color-16.svg new file mode 100644 index 0000000000..644b9a9c61 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Check.imageset/Check-Color-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Check.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Check.imageset/Contents.json new file mode 100644 index 0000000000..14f73a2877 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Check.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Check-Color-16.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Contents.json index c4a79c7d54..f1d879d17e 100644 --- a/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Icon 22.pdf", + "filename" : "Exclamation-High-Color-16.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Exclamation-High-Color-16.svg b/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Exclamation-High-Color-16.svg new file mode 100644 index 0000000000..67505545b4 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Exclamation-High-Color-16.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Icon 22.pdf b/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Icon 22.pdf deleted file mode 100644 index 97d2e7db89..0000000000 Binary files a/DuckDuckGo/Assets.xcassets/Images/CriticalUpdateNotificationInfo.imageset/Icon 22.pdf and /dev/null differ diff --git a/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Contents.json index 552be25080..e224dcfc1c 100644 --- a/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Document-Color-16.pdf", + "filename" : "Release-Notes-Color-16.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Document-Color-16.pdf b/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Document-Color-16.pdf deleted file mode 100644 index 5db4a4627f..0000000000 Binary files a/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Document-Color-16.pdf and /dev/null differ diff --git a/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Release-Notes-Color-16.svg b/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Release-Notes-Color-16.svg new file mode 100644 index 0000000000..440fa7505c --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/ReleaseNotesIndicator.imageset/Release-Notes-Color-16.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Contents.json index 8976fe4303..a68acc82ac 100644 --- a/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Icon 19.pdf", + "filename" : "Exclamation-Color-16-2.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Exclamation-Color-16-2.svg b/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Exclamation-Color-16-2.svg new file mode 100644 index 0000000000..39ad1e8ec7 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Exclamation-Color-16-2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Icon 19.pdf b/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Icon 19.pdf deleted file mode 100644 index 64eb71ba5f..0000000000 Binary files a/DuckDuckGo/Assets.xcassets/Images/UpdateNotificationInfo.imageset/Icon 19.pdf and /dev/null differ diff --git a/DuckDuckGo/Common/Extensions/NSViewExtension.swift b/DuckDuckGo/Common/Extensions/NSViewExtension.swift index 2e81660afc..c17edf489f 100644 --- a/DuckDuckGo/Common/Extensions/NSViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSViewExtension.swift @@ -85,6 +85,12 @@ extension NSView { set { isHidden = !newValue } } + var isVisible: Bool { + guard !isHiddenOrHasHiddenAncestor, + let window, window.isVisible else { return false } + return true + } + func makeMeFirstResponder() { guard let window = window else { Logger.general.error("\(self.className): Window not available") diff --git a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift index 090e9c3121..9a89e51e4f 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift @@ -119,7 +119,7 @@ extension NSPopover { // https://app.asana.com/0/1201037661562251/1206407295280737/f @objc(swizzled_showRelativeToRect:ofView:preferredEdge:) private dynamic func swizzled_show(relativeTo positioningRect: NSRect, of positioningView: NSView, preferredEdge: NSRectEdge) { - if positioningView.superview == nil { + if positioningView.window == nil { var observer: Cancellable? observer = positioningView.observe(\.window) { positioningView, _ in if positioningView.window != nil { diff --git a/DuckDuckGo/Common/Extensions/XMLNodeExtension.swift b/DuckDuckGo/Common/Extensions/XMLNodeExtension.swift new file mode 100644 index 0000000000..86e1829aad --- /dev/null +++ b/DuckDuckGo/Common/Extensions/XMLNodeExtension.swift @@ -0,0 +1,27 @@ +// +// XMLNodeExtension.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. +// + +extension XMLNode { + + func childIfExists(at index: Int) -> XMLNode? { + assert(index >= 0) + guard childCount > index else { return nil } + return child(at: index) + } + +} diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index ac7f6cd5b0..492357259b 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -693,7 +693,7 @@ struct UserText { static let autofill = NSLocalizedString("preferences.autofill", value: "Passwords", comment: "Show Autofill preferences") static let aboutDuckDuckGo = NSLocalizedString("preferences.about.about-duckduckgo", value: "About DuckDuckGo", comment: "About screen") - static let privacySimplified = NSLocalizedString("preferences.about.privacy-simplified", value: "Privacy, simplified.", comment: "About screen") + static let duckduckgoTagline = NSLocalizedString("preferences.about.duckduckgo-tagline", value: "Your protection, our priority.", comment: "About screen") static let aboutUnsupportedDeviceInfo1 = NSLocalizedString("preferences.about.unsupported-device-info1", value: "DuckDuckGo is no longer providing browser updates for your version of macOS.", comment: "This string represents a message informing the user that DuckDuckGo is no longer providing browser updates for their version of macOS") static func aboutUnsupportedDeviceInfo2(version: String) -> String { let localized = NSLocalizedString("preferences.about.unsupported-device-info2", value: "Please update to macOS %@ or later to use the most recent version of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates.", comment: "Copy in section that tells the user to update their macOS version since their current version is unsupported") @@ -949,8 +949,8 @@ struct UserText { static let bitwardenError = NSLocalizedString("bitwarden.error", value: "Unable to find or connect to Bitwarden", comment: "This message appears when the application is unable to find or connect to Bitwarden, indicating a connection issue.") static let bitwardenNotInstalled = NSLocalizedString("bitwarden.not.installed", value: "Bitwarden app is not installed", comment: "") static let bitwardenOldVersion = NSLocalizedString("bitwarden.old.version", value: "Please update Bitwarden to the latest version", comment: "Message that warns user they need to update their password manager Bitwarden app vesion") - static let bitwardenIncompatible = NSLocalizedString("bitwarden.incompatible", value: "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:", comment: "Message that warns user that specific Bitwarden app vesions are not compatible with this app") - static let bitwardenIncompatibleStep1 = NSLocalizedString("bitwarden.incompatible.step.1", value: "Download v2024.4.3", comment: "First step to downgrade Bitwarden") + static let bitwardenIncompatible = NSLocalizedString("bitwarden.incompatible", value: "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.10.0, v2024.10.1, v2024.10.2. Please downgrade to an older version by following these steps:", comment: "Message that warns user that specific Bitwarden app vesions are not compatible with this app") + static let bitwardenIncompatibleStep1 = NSLocalizedString("bitwarden.incompatible.step.1", value: "Download v2024.9.0", comment: "First step to downgrade Bitwarden") static let bitwardenIncompatibleStep2 = NSLocalizedString("bitwarden.incompatible.step.2", value: "2. Open the downloaded DMG file and drag the Bitwarden application to\nthe /Applications folder.", comment: "Second step to downgrade Bitwarden") static let bitwardenIntegrationNotApproved = NSLocalizedString("bitwarden.integration.not.approved", value: "Integration with DuckDuckGo is not approved in Bitwarden app", comment: "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates that the integration with DuckDuckGo has not been approved in the Bitwarden app.") static let bitwardenMissingHandshake = NSLocalizedString("bitwarden.missing.handshake", value: "Missing handshake", comment: "While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates a missing handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information).") @@ -1182,8 +1182,8 @@ struct UserText { static let bookmarksBarPromptAccept = NSLocalizedString("bookmarks.bar.prompt.accept", value: "Show", comment: "Accept button label on bookmarks bar prompt") // MARK: Home Page Settings - static let homePageSettingsOnboardingTitle = NSLocalizedString("home.page.settings.onboarding.title", value: "Add extra personality to your new tab page", comment: "Home Page Settings Onboarding message title") - static let homePageSettingsOnboardingMessage = NSLocalizedString("home.page.settings.onboarding.message", value: "Customize the background, theme, and even what content you see. Give it a try!", comment: "Home Page Settings Onboarding message") + static let homePageSettingsOnboardingTitle = NSLocalizedString("home.page.settings.onboarding.title", value: "New search box, custom backgrounds & more!", comment: "Home Page Settings Onboarding message title") + static let homePageSettingsOnboardingMessage = NSLocalizedString("home.page.settings.onboarding.message", value: "Add extra personality and pick what you want to see on your new tab page. Give it a try!", comment: "Home Page Settings Onboarding message") static let homePageSettingsTitle = NSLocalizedString("home.page.settings.header", value: "Customize", comment: "Home Page Settings title") static let goToSettings = NSLocalizedString("home.page.settings.go.to.settings", value: "Go to Settings", comment: "Settings button caption") static let background = NSLocalizedString("home.page.settings.background", value: "Background", comment: "Section title in Home Page Settings to customization Home Page background") @@ -1237,21 +1237,29 @@ struct UserText { static let downloadsOpenDownloadsFolder = NSLocalizedString("downloads.open-downloads-folder", value: "Open Downloads Folder", comment: "Button in the downloads manager that allows the user to open the downloads folder") // MARK: Updates - static let updateAvailableMenuItem = NSLocalizedString("update.available.menu.item", value: "Update Available - Restart Now", comment: "Title of the menu item that informs user that a new update is available. Clicking on the menu item restarts the app and installs the update") + static let updateAvailableMenuItem = NSLocalizedString("update.available.menu.item", value: "Update Available - Install Now", comment: "Title of the menu item that informs user that a new update is available. Clicking on the menu item installs the update") static let releaseNotesMenuItem = NSLocalizedString("release.notes.menu.item", value: "Release Notes", comment: "Title of the dialog menu item that opens release notes") static let whatsNewMenuItem = NSLocalizedString("whats.new.menu.item", value: "What's New", comment: "Title of the dialog menu item that opens the 'What's New' page") static let browserUpdatesTitle = NSLocalizedString("settings.browser.updates.title", value: "Browser Updates", comment: "Title of the section in Settings where people set up automatic vs manual updates") static let automaticUpdates = NSLocalizedString("settings.automatic.updates", value: "Automatically install updates (recommended)", comment: "Title of the checkbox item to set up automatic updates of the browser") static let manualUpdates = NSLocalizedString("settings.manual.updates", value: "Check for updates but let you choose to install them", comment: "Title of the checkbox item to set up manual updates of the browser") static let checkingForUpdate = NSLocalizedString("settings.checking.for.update", value: "Checking for update", comment: "Label informing users the app is currently checking for new update") + static let downloadingUpdate = NSLocalizedString("settings.downloading.update", value: "Downloading update %@", comment: "Label informing users the app is currently downloading the update. This will contain a percentage") + static let preparingUpdate = NSLocalizedString("settings.preparing.update", value: "Preparing update", comment: "Label informing users the app is preparing to update.") + static let updateFailed = NSLocalizedString("settings.update.failed", value: "Update failed", comment: "Label informing users the app is unable to update.") static let upToDate = NSLocalizedString("settings.up.to.date", value: "DuckDuckGo is up to date", comment: "Label informing users the app is currently up to date and no update is required.") static let newerVersionAvailable = NSLocalizedString("settings.newer.version.available", value: "Newer version available", comment: "Label informing users the newer version of the app is available to install.") + static let newerCriticalUpdateAvailable = NSLocalizedString("settings.newer.critical.update.available", value: "Critical update needed", comment: "Label informing users the critical update of the app is available to install.") static let lastChecked = NSLocalizedString("settings.last.checked", value: "Last checked", comment: "Label informing users what is the last time the app checked for the update.") - static let restartToUpdate = NSLocalizedString("settings.restart.to.update", value: "Restart to Update", comment: "Button label trigering restart and update of the application.") + static let restartToUpdate = NSLocalizedString("settings.restart.to.update", value: "Restart To Update", comment: "Button label triggering restart and update of the application.") + static let runUpdate = NSLocalizedString("settings.run.update", value: "Update DuckDuckGo", comment: "Button label triggering update of the application.") + static let retryUpdate = NSLocalizedString("settings.retry.update", value: "Retry Update", comment: "Button label triggering a retry of the update.") static let browserUpdatedNotification = NSLocalizedString("notification.browser.updated", value: "Browser Updated", comment: "Notification informing user the app has been updated") static let browserDowngradedNotification = NSLocalizedString("notification.browser.downgraded", value: "Browser Downgraded", comment: "Notification informing user the app has been downgraded") - static let criticalUpdateNotification = NSLocalizedString("notification.critical.update", value: "Critical update required. Restart to update.", comment: "Notification informing user a critical update is required.") - static let updateAvailableNotification = NSLocalizedString("notification.update.available", value: "New version available. Restart to update.", comment: "Notification informing user the a version of app is available") + static let criticalUpdateNotification = NSLocalizedString("notification.critical.update", value: "Critical update needed.", comment: "Notification informing user a critical update is available.") + static let updateAvailableNotification = NSLocalizedString("notification.update.available", value: "New version available.", comment: "Notification informing user the a version of app is available.") + static let autoUpdateAction = NSLocalizedString("notification.auto.update.action", value: "Restart to update.", comment: "Action to take when an automatic update is available.") + static let manualUpdateAction = NSLocalizedString("notification.manual.update.action", value: "Click here to update.", comment: "Action to take when a manual update is available.") static let viewDetails = NSLocalizedString("view.details.button", value: "View Details", comment: "Button title to open more details about the update") enum Bookmarks { @@ -1395,4 +1403,6 @@ struct UserText { // Key: "home.page.promotion.freemium.dbp.post.scan.engagement.button.title" // Comment: "Title for the Freemium DBP Home Page Post Scan Engagement Promotion Button" static let homePagePromotionFreemiumDBPPostScanEngagementButtonTitle = "View Results" + + static let removeSuggestionTooltip = NSLocalizedString("remove.suggestion.tooltip", value: "Remove from browsing history", comment: "Tooltip for the button which removes the history entry from the history") } diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 508b4ff1fd..a18850b028 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -184,6 +184,7 @@ public struct UserDefaultsWrapper { // Updates case automaticUpdates = "updates.automatic" + case pendingUpdateShown = "pending.update.shown" // Experiments case pixelExperimentInstalled = "pixel.experiment.installed" diff --git a/DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift b/DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift index 5524315669..305ae118f0 100644 --- a/DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift +++ b/DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift @@ -141,9 +141,9 @@ final class BookmarkHTMLReader { private func validateHTMLBookmarksDocument(_ document: XMLDocument) throws -> XMLNode? { let root = document.rootElement() - guard let body = root?.child(at: 1) else { throw ImportError(type: .validationBody, underlyingError: nil) } + guard let body = root?.childIfExists(at: 1) else { throw ImportError(type: .validationBody, underlyingError: nil) } // get /html/body/*[0] - let cursor = body.child(at: 0) + let cursor = body.childIfExists(at: 0) return cursor } @@ -155,12 +155,12 @@ final class BookmarkHTMLReader { switch cursor?.htmlTag { case .dl: let originalCursorValue = cursor - cursor = cursor?.child(at: 0) + cursor = cursor?.childIfExists(at: 0) dlLoop: while cursor != nil { switch cursor?.htmlTag { case .dd: - if cursor?.child(at: 0)?.htmlTag == .h3 { - cursor = cursor?.child(at: 0) + if cursor?.childIfExists(at: 0)?.htmlTag == .h3 { + cursor = cursor?.childIfExists(at: 0) break dlLoop } cursor = cursor?.nextSibling @@ -195,7 +195,7 @@ final class BookmarkHTMLReader { itemType = cursor?.itemType(inSafariFormat: false) switch itemType { case .some: - cursor = cursor?.child(at: 0) + cursor = cursor?.childIfExists(at: 0) case .none: cursor = cursor?.nextSibling } @@ -243,12 +243,12 @@ final class BookmarkHTMLReader { private func readFolderContents(_ node: XMLNode?) throws -> [ImportedBookmarks.BookmarkOrFolder] { var cursor = node - cursor = cursor?.child(at: 0) + cursor = cursor?.childIfExists(at: 0) var children = [ImportedBookmarks.BookmarkOrFolder]() while cursor != nil { - let firstChild = cursor?.child(at: 0) + let firstChild = cursor?.childIfExists(at: 0) switch (cursor?.htmlTag, firstChild?.htmlTag) { case (.dd, .h3): children.append(try readFolder(firstChild)) @@ -354,13 +354,13 @@ private extension XMLNode { return .folder case .dt: return .bookmark - case .dl where child(at: 0)?.child(at: 0)?.htmlTag == .a: + case .dl where childIfExists(at: 0)?.childIfExists(at: 0)?.htmlTag == .a: return .safariTopLevelBookmarks default: return nil } } else { - switch (htmlTag, child(at: 0)?.htmlTag) { + switch (htmlTag, childIfExists(at: 0)?.htmlTag) { case (.dd, .h3): return .folder case (.dt, .a): diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 918c1bea94..ad91e2e3be 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -7612,55 +7612,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" + "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.10.0, v2024.10.1, v2024.10.2. Please downgrade to an older version by following these steps:" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "The following Bitwarden versions are incompatible with DuckDuckGo: v2024.3.0, v2024.3.2, v2024.4.0, v2024.4.1. Please update to a newer version by following these steps:" } } @@ -7672,55 +7672,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Download v2024.4.3" + "value" : "Download v2024.9.0" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Download v2024.4.3" } } @@ -27359,55 +27359,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Passe den Hintergrund, das Design und sogar die angezeigten Inhalte an. Versuch es doch mal!" + "value" : "Personalisiere deine neue Tab-Seite. Probier’s doch mal aus!" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Customize the background, theme, and even what content you see. Give it a try!" + "value" : "Add extra personality and pick what you want to see on your new tab page. Give it a try!" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Personaliza el fondo, el tema e incluso el contenido que ves. ¡Pruébalo!" + "value" : "Añade más personalidad y elige lo que quieres ver en tu nueva pestaña. ¡Pruébalo!" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Personnalisez l'arrière-plan, le thème et même le contenu que vous voyez. Essayez par vous-même !" + "value" : "Ajoutez une personnalité supplémentaire et choisissez ce que vous voulez voir sur votre nouvelle page à onglet. Essayez par vous-même !" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Personalizza lo sfondo, il tema e persino i contenuti che vedi. Prova!" + "value" : "Aggiungi un tocco di personalità e scegli cosa vuoi vedere nella tua nuova scheda. Prova!" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pas de achtergrond, het thema en zelfs de inhoud aan die je ziet. Probeer het maar eens!" + "value" : "Voeg extra persoonlijkheid toe en kies wat je op je nieuwe tabbladpagina wilt zien. Probeer het maar eens!" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dostosuj tło, motyw oraz wyświetlaną zawartość. Spróbuj!" + "value" : "Wybierz, co chcesz zobaczyć na stronie nowej karty, aby zyskała bardziej indywidualny charakter. Spróbuj!" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Personaliza o fundo, o tema e até o conteúdo que vês. Experimenta!" + "value" : "Adiciona personalidade extra e escolhe o que queres ver na tua página do novo separador. Experimenta!" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Можно настроить фон, тему и даже отображаемый контент. Давайте попробуем!" + "value" : "Добавьте дополнительную персонализацию и выберите, что должно присутствовать на странице новой вкладки. Попробуйте!" } } } @@ -27419,55 +27419,55 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Personalisiere deine neue Tab-Seite" + "value" : "Neues Suchfeld, benutzerdefinierte Hintergründe und mehr!" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Add extra personality to your new tab page" + "value" : "New search box, custom backgrounds & more!" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Añade más personalidad a tu página de nueva pestaña" + "value" : "¡Nuevo cuadro de búsqueda, fondos personalizados y mucho más!" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ajoutez une personnalité supplémentaire à votre nouvelle page à onglet" + "value" : "Nouvelle boîte de recherche, arrière-plans personnalisés et plus encore !" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Aggiungi un tocco di personalità alla tua nuova scheda" + "value" : "Nuova casella di ricerca, sfondi personalizzati e altro ancora!" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Voeg extra persoonlijkheid toe aan je nieuwe tabbladpagina" + "value" : "Nieuw zoekvak, aangepaste achtergronden en meer!" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dostosuj stronę nowej zakładki do potrzeb" + "value" : "Nowe pole wyszukiwania, niestandardowe tła i nie tylko!" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Adiciona personalidade extra à tua página do novo separador" + "value" : "Nova caixa de pesquisa, fundos personalizados e mais!" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Дополнительная персонализация новой вкладки" + "value" : "Новая строка поиска, собственные фоны и многое другое" } } } @@ -37881,6 +37881,66 @@ } } }, + "notification.auto.update.action" : { + "comment" : "Action to take when an automatic update is available.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zum Aktualisieren neu starten." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Restart to update." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reinicia para actualizar." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Redémarrer pour mettre à jour." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riavvia per aggiornare." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start opnieuw om bij te werken." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uruchom ponownie, aby zaktualizować." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reiniciar para atualizar." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перезапустите приложение." + } + } + } + }, "notification.badge.cookiesmanaged" : { "comment" : "Notification that appears when browser automatically handle cookies", "extractionState" : "extracted_with_value", @@ -38122,121 +38182,181 @@ } }, "notification.critical.update" : { - "comment" : "Notification informing user a critical update is required.", + "comment" : "Notification informing user a critical update is available.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kritisches Update erforderlich." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Critical update needed." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se necesita una actualización crítica." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Une mise à jour critique est nécessaire." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiornamento critico necessario." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kritieke update nodig." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wymagana krytyczna aktualizacja." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualização crítica necessária." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Требуется критическое обновление." + } + } + } + }, + "notification.manual.update.action" : { + "comment" : "Action to take when a manual update is available.", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kritisches Update erforderlich. Zum Aktualisieren neu starten." + "value" : "Klicke hier, um zu aktualisieren." } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Critical update required. Restart to update." + "value" : "Click here to update." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Se requiere una actualización crítica. Reinicia para actualizar." + "value" : "Haz clic aquí para actualizar." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Une mise à jour critique est requise. Redémarrer pour mettre à jour." + "value" : "Cliquez ici pour mettre à jour." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Aggiornamento critico obbligatorio. Riavvia per aggiornare." + "value" : "Fai clic qui per aggiornare." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kritieke update vereist. Start opnieuw om bij te werken." + "value" : "Klik hier om te updaten." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wymagana krytyczna aktualizacja. Uruchom ponownie, aby zaktualizować." + "value" : "Kliknij tutaj, aby zaktualizować." } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Atualização crítica obrigatória. Reiniciar para atualizar." + "value" : "Clica aqui para atualizar." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Требуется критическое обновление. Перезапустите приложение." + "value" : "Нажмите здесь, чтобы обновить." } } } }, "notification.update.available" : { - "comment" : "Notification informing user the a version of app is available", + "comment" : "Notification informing user the a version of app is available.", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Neue Version verfügbar. Zum Aktualisieren neu starten." + "value" : "Neue Version verfügbar." } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "New version available. Restart to update." + "value" : "New version available." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Nueva versión disponible. Reinicia para actualizar." + "value" : "Nueva versión disponible." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Une nouvelle version est disponible. Redémarrer pour mettre à jour." + "value" : "Une nouvelle version est disponible." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nuova versione disponibile. Riavvia per aggiornare." + "value" : "Nuova versione disponibile." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Nieuwe versie beschikbaar. Start opnieuw om bij te werken." + "value" : "Nieuwe versie beschikbaar." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dostępna nowa wersja. Uruchom ponownie, aby zaktualizować." + "value" : "Dostępna nowa wersja." } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Nova versão disponível. Reiniciar para atualizar." + "value" : "Nova versão disponível." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Доступна новая версия. Перезапустите приложение." + "value" : "Доступна новая версия." } } } @@ -38787,55 +38907,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Du bist bereit! Du kannst mich jederzeit im Dock antreffen.\nMöchtest du sehen, wie ich dich beschütze? Versuche, eine deiner Lieblingsseiten zu besuchen 👆\n\nBehalte die Adressleiste im Auge. Ich werde Tracker blockieren und die Sicherheit deiner Verbindung verbessern, wenn möglichu{00A0}🔒" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒" + "value" : "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible 🔒" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "¡Ya está todo listo! Puedes encontrarme en el Dock en cualquier momento.\n¿Quieres ver cómo te protejo? Prueba a visitar uno de tus sitios favoritos 👆\n\nNo pierdas de vista la barra de direcciones al navegar. Bloquearé los rastreadores y mejoraré la seguridad de tu conexión cuando sea posible{00A0}🔒" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Tout est prêt ! Vous pouvez me trouver sur le Dock à tout moment.\nVous voulez voir comment je vous protège ? Essayez de visiter l'un de vos sites préférés 👆\n\nContinuez à regarder la barre d'adresse au fur et à mesure. Je bloquerai les traqueurs et mettrai à niveau la sécurité de votre connexion si possible 🔒" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Tutto pronto! Puoi trovarmi nel dock in qualsiasi momento.\nVuoi vedere come ti proteggo? Prova a visitare uno dei tuoi siti preferiti 👆\n\nContinua a controllare la barra degli indirizzi mentre esplori. Bloccherò i sistemi di tracciamento e aggiornerò la sicurezza della tua connessione quando possibile{00A0} 🔒" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Je bent helemaal klaar! Je kunt me altijd vinden in het Dock.\nWil je zien hoe ik je bescherm? Ga eens naar een van je favoriete websites 👆\n\nKijk tijdens het surfen goed naar de adresbalk. Ik blokkeer trackers en werk de beveiliging van je verbinding bij wanneer mogelijk 🔒" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Wszystko gotowe! W każdej chwili możesz mnie znaleźć w Docku.\nChcesz zobaczyć, jak Cię chronię? Spróbuj odwiedzić jedną z ulubionych stron 👆\n\nW międzyczasie obserwuj pasek adresu. Będę blokować mechanizmy śledzące i w miarę możliwości poprawiać bezpieczeństwo połączenia 🔒" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Está tudo pronto! Podes encontrar-me na Dock em qualquer altura.\nQueres ver como te protejo? Experimenta visitar um dos teus sites favoritos 👆\n\nContinua a observar a barra de endereço à medida que vais avançando. Vou bloquear os rastreadores e melhorar a segurança da tua ligação sempre que possível 🔒" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Готово! Теперь меня всегда можно найти на док-панели.\nВам интересно, как я защищаю вашу конфиденциальность? Зайдите на свой любимый сайт...👆\n\nИ следите за адресной строкой. По возможности я заблокирую все трекеры и сделаю соединение более безопасным {00A0}🔒" } } @@ -38847,55 +38967,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Du bist bereit!\n\nMöchtest du sehen, wie ich dich beschütze? Versuche, eine deiner Lieblingsseiten zu besuchen 👆\n\nBehalte die Adressleiste im Auge. Ich werde Tracker blockieren und die Sicherheit deiner Verbindung verbessern, wenn möglich 🔒" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "You’re all set!\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒" + "value" : "You’re all set!\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible 🔒" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "¡Ya está todo listo!\n\n¿Quieres ver cómo te protejo? Prueba a visitar uno de tus sitios favoritos 👆\n\nSigue viendo la barra de direcciones sobre la marcha. Bloquearé los rastreadores y mejoraré la seguridad de tu conexión cuando sea posible 🔒" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Tout est prêt !\n\nVous voulez voir comment je vous protège ? Essayez de visiter l'un de vos sites préférés 👆\n\n Continuez à regarder la barre d'adresse au fur et à mesure. Je bloquerai les traqueurs et mettrai à niveau la sécurité de votre connexion si possible 🔒" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Tutto pronto.\n\nVuoi vedere come ti proteggo? Prova a visitare uno dei tuoi siti preferiti 👆Continua a controllare la barra degli indirizzi mentre esplori. Bloccherò i sistemi di tracciamento e aggiornerò la sicurezza della tua connessione quando possibile 🔒" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Je bent helemaal klaar!\n\nWil je zien hoe ik je bescherm? Ga eens naar een van je favoriete websites 👆 \n\n Kijk tijdens het surfen goed naar de adresbalk. Ik blokkeer trackers en werk de beveiliging van je verbinding bij wanneer mogelijk 🔒" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Wszystko gotowe!\n\nChcesz zobaczyć, jak Cię chronię? Spróbuj odwiedzić jedną z ulubionych stron 👆\n\nW międzyczasie obserwuj pasek adresu. Będę blokować mechanizmy śledzące i w miarę możliwości poprawiać bezpieczeństwo połączenia 🔒" } }, "pt" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Estás tudo pronto!\n\nQueres ver como te protejo? Experimenta visitar um dos teus sites favoritos 👆\n\nContinua a observar a barra de endereço à medida que vais avançando. Vou bloquear os rastreadores e melhorar a segurança da tua ligação sempre que possível 🔒" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Все готово!\n\nХотите увидеть, как я вас защищаю? Зайдите на один из любимых сайтов 👆\n\nСледите за адресной строкой. Я по возможности буду блокировать трекеры и обеспечивать вам более безопасное соединение 🔒" } } @@ -44206,6 +44326,18 @@ } } }, + "pinning.hide-aichat-shortcut" : { + "comment" : "Menu item for hiding the AI Chat shortcut", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Hide AI Chat Shortcut" + } + } + } + }, "pinning.hide-autofill-shortcut" : { "comment" : "Menu item for hiding the passwords shortcut", "extractionState" : "extracted_with_value", @@ -44446,6 +44578,18 @@ } } }, + "pinning.show-aichat-shortcut" : { + "comment" : "Menu item for showing the AI Chat shortcut", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show AI Chat Shortcut" + } + } + } + }, "pinning.show-autofill-shortcut" : { "comment" : "Menu item for showing the passwords shortcut", "extractionState" : "extracted_with_value", @@ -50858,6 +51002,66 @@ } } }, + "preferences.about.duckduckgo-tagline" : { + "comment" : "About screen", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein Schutz, unsere Priorität." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Your protection, our priority." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tu protección, nuestra prioridad." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre protection, notre priorité." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "La tua protezione, la nostra priorità." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jouw bescherming, onze prioriteit." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje bezpieczeństwo jest naszym priorytetem." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A tua proteção, a nossa prioridade." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваша защита — наш приоритет." + } + } + } + }, "preferences.about.more-at" : { "comment" : "Link to the about page", "extractionState" : "extracted_with_value", @@ -50980,7 +51184,7 @@ }, "preferences.about.privacy-simplified" : { "comment" : "About screen", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -56741,6 +56945,66 @@ } } }, + "settings.downloading.update" : { + "comment" : "Label informing users the app is currently downloading the update. This will contain a percentage", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update wird heruntergeladen %@" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Downloading update %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descargando actualización %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Téléchargement de la mise à jour %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Download dell'aggiornamento %@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update %@ downloaden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pobieranie aktualizacji %@" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A transferir a atualização %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Загрузка обновления %@" + } + } + } + }, "settings.last.checked" : { "comment" : "Label informing users what is the last time the app checked for the update.", "extractionState" : "extracted_with_value", @@ -56861,6 +57125,66 @@ } } }, + "settings.newer.critical.update.available" : { + "comment" : "Label informing users the critical update of the app is available to install.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kritisches Update erforderlich" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Critical update needed" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se necesita una actualización crítica" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Une mise à jour critique est nécessaire" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiornamento critico necessario" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kritieke update nodig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wymagana krytyczna aktualizacja" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualização crítica necessária" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Требуется критическое обновление" + } + } + } + }, "settings.newer.version.available" : { "comment" : "Label informing users the newer version of the app is available to install.", "extractionState" : "extracted_with_value", @@ -56921,8 +57245,68 @@ } } }, + "settings.preparing.update" : { + "comment" : "Label informing users the app is preparing to update.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update vorbereiten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Preparing update" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preparando la actualización" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Préparation de la mise à jour" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preparazione dell'aggiornamento" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update voorbereiden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przygotowanie aktualizacji" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A preparar a atualização" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подготовка обновления" + } + } + } + }, "settings.restart.to.update" : { - "comment" : "Button label trigering restart and update of the application.", + "comment" : "Button label triggering restart and update of the application.", "extractionState" : "extracted_with_value", "localizations" : { "de" : { @@ -56934,7 +57318,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Restart to Update" + "value" : "Restart To Update" } }, "es" : { @@ -56981,6 +57365,126 @@ } } }, + "settings.retry.update" : { + "comment" : "Button label triggering a retry of the update.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update nochmal versuchen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Retry Update" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reintentar actualización" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réessayer la mise à jour" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riprova l'aggiornamento" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update opnieuw proberen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ponów próbę aktualizacji" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repetir atualização" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повторить попытку обновления" + } + } + } + }, + "settings.run.update" : { + "comment" : "Button label triggering update of the application.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo aktualisieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Update DuckDuckGo" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar DuckDuckGo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mettre à jour DuckDuckGo" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiorna DuckDuckGo" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo bijwerken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaktualizuj DuckDuckGo" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualizar o DuckDuckGo" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обновить DuckDuckGo" + } + } + } + }, "settings.up.to.date" : { "comment" : "Label informing users the app is currently up to date and no update is required.", "extractionState" : "extracted_with_value", @@ -57041,6 +57545,66 @@ } } }, + "settings.update.failed" : { + "comment" : "Label informing users the app is unable to update.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update fehlgeschlagen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Update failed" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualización fallida" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Échec de la mise à jour" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiornamento non riuscito" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update mislukt" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niepowodzenie aktualizacji" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "A atualização falhou" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось обновить" + } + } + } + }, "share.menu.item" : { "comment" : "Menu item title", "extractionState" : "extracted_with_value", @@ -61635,61 +62199,61 @@ } }, "update.available.menu.item" : { - "comment" : "Title of the menu item that informs user that a new update is available. Clicking on the menu item restarts the app and installs the update", + "comment" : "Title of the menu item that informs user that a new update is available. Clicking on the menu item installs the update", "extractionState" : "extracted_with_value", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Update verfügbar – Jetzt neu starten" + "value" : "Update verfügbar – jetzt installieren" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "Update Available - Restart Now" + "value" : "Update Available - Install Now" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Actualización disponible - Reiniciar ahora" + "value" : "Actualización disponible - Instalar ahora" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Mise à jour disponible : redémarrer" + "value" : "Mise à jour disponible : installer" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Aggiornamento disponibile - Riavvia ora" + "value" : "Aggiornamento disponibile - Installa ora" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Update beschikbaar - nu opnieuw opstarten" + "value" : "Update beschikbaar - nu installeren" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dostępna aktualizacja — uruchom ponownie teraz" + "value" : "Dostępna aktualizacja — zainstaluj teraz" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Atualização disponível – Reiniciar agora" + "value" : "Atualização disponível – Instalar agora" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Доступно обновление: перезапустить" + "value" : "Доступно обновление – установить сейчас" } } } @@ -62878,4 +63442,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DuckDuckGo/MainWindow/MainWindowController.swift b/DuckDuckGo/MainWindow/MainWindowController.swift index 5911d9c289..a21f293ae6 100644 --- a/DuckDuckGo/MainWindow/MainWindowController.swift +++ b/DuckDuckGo/MainWindow/MainWindowController.swift @@ -74,8 +74,12 @@ final class MainWindowController: NSWindowController { return false #elseif REVIEW if Application.runType == .uiTests { + Application.appDelegate.onboardingStateMachine.state = .onboardingCompleted return false } else { + if Application.runType == .uiTestsOnboarding { + Application.appDelegate.onboardingStateMachine.state = .onboardingCompleted + } let onboardingIsComplete = OnboardingViewModel.isOnboardingFinished || LocalStatisticsStore().waitlistUnlocked return !onboardingIsComplete } diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index e3f67e7aa9..bdf08d8e44 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -611,6 +611,7 @@ final class MainMenu: NSMenu { NSMenuItem(title: "150 Tabs", action: #selector(MainViewController.addDebugTabs(_:)), representedObject: 150) } } + NSMenuItem(title: "Skip Onboarding", action: #selector(MainViewController.skipOnboarding)) NSMenuItem(title: "Reset Data") { NSMenuItem(title: "Reset Default Browser Prompt", action: #selector(MainViewController.resetDefaultBrowserPrompt)) NSMenuItem(title: "Reset Default Grammar Checks", action: #selector(MainViewController.resetDefaultGrammarChecks)) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 197e3ae0dd..00f780c18b 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -849,6 +849,13 @@ extension MainViewController { UserDefaults.standard.set(true, forKey: UserDefaultsWrapper.Key.homePageShowEmailProtection.rawValue) } + @objc func skipOnboarding(_ sender: Any?) { + UserDefaults.standard.set(true, forKey: UserDefaultsWrapper.Key.onboardingFinished.rawValue) + Application.appDelegate.onboardingStateMachine.state = .onboardingCompleted + WindowControllersManager.shared.updatePreventUserInteraction(prevent: false) + WindowControllersManager.shared.replaceTabWith(Tab(content: .newtab)) + } + @objc func resetOnboarding(_ sender: Any?) { UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.onboardingFinished.rawValue) } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 44e6cf951e..63766e5838 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -61,7 +61,7 @@ final class AddressBarButtonsViewController: NSViewController { @IBOutlet weak var imageButtonWrapper: NSView! @IBOutlet weak var imageButton: NSButton! @IBOutlet weak var clearButton: NSButton! - @IBOutlet weak var buttonsContainer: NSStackView! + @IBOutlet private weak var buttonsContainer: NSStackView! @IBOutlet weak var animationWrapperView: NSView! var trackerAnimationView1: LottieAnimationView! @@ -72,7 +72,7 @@ final class AddressBarButtonsViewController: NSViewController { @IBOutlet weak var notificationAnimationView: NavigationBarBadgeAnimationView! - @IBOutlet weak var permissionButtons: NSView! + @IBOutlet private weak var permissionButtons: NSView! @IBOutlet weak var cameraButton: PermissionButton! { didSet { cameraButton.isHidden = true @@ -368,7 +368,7 @@ final class AddressBarButtonsViewController: NSViewController { return } } - guard button.isShown, permissionButtons.isShown else { return } + guard button.isVisible else { return } (popover.contentViewController as? PermissionAuthorizationViewController)?.query = query popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY) diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 1be1ba6e06..66610ce9b3 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -1234,11 +1234,6 @@ extension AddressBarTextField: SuggestionViewControllerDelegate { navigate(suggestion: suggestion) } - func shouldCloseSuggestionWindow(forMouseEvent event: NSEvent) -> Bool { - // don't hide suggestions if clicking somewhere inside the Address Bar view - return superview?.isMouseLocationInsideBounds(event.locationInWindow) != true - } - } extension Notification.Name { diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 8d05402487..554d665a9e 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -49,7 +49,7 @@ protocol OptionsButtonMenuDelegate: AnyObject { func optionsButtonMenuRequestedIdentityTheftRestoration(_ menu: NSMenu) } -final class MoreOptionsMenu: NSMenu { +final class MoreOptionsMenu: NSMenu, NSMenuDelegate { weak var actionDelegate: OptionsButtonMenuDelegate? @@ -118,6 +118,8 @@ final class MoreOptionsMenu: NSMenu { } self.emailManager.requestDelegate = self + delegate = self + setupMenuItems() } @@ -311,8 +313,10 @@ final class MoreOptionsMenu: NSMenu { private func addUpdateItem() { #if SPARKLE guard NSApp.runType != .uiTests, - let update = Application.appDelegate.updateController.latestUpdate, - !update.isInstalled + let updateController = Application.appDelegate.updateController, + let update = updateController.latestUpdate, + !update.isInstalled, + updateController.updateProgress.isDone else { return } @@ -478,6 +482,14 @@ final class MoreOptionsMenu: NSMenu { return networkProtectionItem } + func menuWillOpen(_ menu: NSMenu) { +#if SPARKLE + guard let updateController = Application.appDelegate.updateController else { return } + if updateController.hasPendingUpdate && updateController.needsNotificationDot { + updateController.needsNotificationDot = false + } +#endif + } } final class EmailOptionsButtonSubMenu: NSMenu { diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenuButton.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenuButton.swift index 4e2934e4be..a81893b20e 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenuButton.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenuButton.swift @@ -40,6 +40,9 @@ final class MoreOptionsMenuButton: MouseOverButton { var isNotificationVisible: Bool = false { didSet { updateNotificationVisibility() +#if SPARKLE + needsDisplay = isNotificationVisible != oldValue +#endif } } @@ -50,8 +53,8 @@ final class MoreOptionsMenuButton: MouseOverButton { if NSApp.runType != .uiTests { updateController = Application.appDelegate.updateController } -#endif subscribeToUpdateInfo() +#endif } override func updateLayer() { @@ -61,10 +64,11 @@ final class MoreOptionsMenuButton: MouseOverButton { private func subscribeToUpdateInfo() { #if SPARKLE - cancellable = updateController?.isUpdateAvailableToInstallPublisher + guard let updateController else { return } + cancellable = Publishers.CombineLatest(updateController.hasPendingUpdatePublisher, updateController.notificationDotPublisher) .receive(on: DispatchQueue.main) - .sink { [weak self] isAvailable in - self?.isNotificationVisible = isAvailable + .sink { [weak self] hasPendingUpdate, needsNotificationDot in + self?.isNotificationVisible = hasPendingUpdate && needsNotificationDot } #endif } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard index 3e5fa0e070..2fb09b93d1 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard +++ b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard @@ -621,7 +621,7 @@ + - + + + + + + + + @@ -176,7 +207,9 @@ + + @@ -224,7 +257,7 @@ - + @@ -285,6 +318,7 @@ + diff --git a/DuckDuckGo/Suggestions/View/SuggestionTableCellView.swift b/DuckDuckGo/Suggestions/View/SuggestionTableCellView.swift index 2f7a2823f0..a66ac1a3af 100644 --- a/DuckDuckGo/Suggestions/View/SuggestionTableCellView.swift +++ b/DuckDuckGo/Suggestions/View/SuggestionTableCellView.swift @@ -19,6 +19,7 @@ import Cocoa import Common import os.log +import Suggestions final class SuggestionTableCellView: NSTableCellView { @@ -31,25 +32,39 @@ final class SuggestionTableCellView: NSTableCellView { static let selectedTintColor: NSColor = .selectedSuggestionTint @IBOutlet weak var iconImageView: NSImageView! + @IBOutlet weak var removeButton: NSButton! @IBOutlet weak var suffixTextField: NSTextField! + @IBOutlet weak var suffixTrailingConstraint: NSLayoutConstraint! + + var suggestion: Suggestion? override func awakeFromNib() { suffixTextField.textColor = Self.suffixColor + removeButton.toolTip = UserText.removeSuggestionTooltip + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + + updateDeleteImageViewVisibility() } var isSelected: Bool = false { didSet { - updateIconImageView() + updateImageViews() updateTextField() + updateDeleteImageViewVisibility() } } var isBurner: Bool = false func display(_ suggestionViewModel: SuggestionViewModel) { + self.suggestion = suggestionViewModel.suggestion attributedString = suggestionViewModel.tableCellViewAttributedString iconImageView.image = suggestionViewModel.icon suffixTextField.stringValue = suggestionViewModel.suffix + setRemoveButtonHidden(true) updateTextField() } @@ -76,8 +91,28 @@ final class SuggestionTableCellView: NSTableCellView { } } - private func updateIconImageView() { + private func updateImageViews() { iconImageView.contentTintColor = isSelected ? Self.selectedTintColor : Self.iconColor + removeButton.contentTintColor = isSelected ? Self.selectedTintColor : Self.iconColor + } + + func updateDeleteImageViewVisibility() { + guard let window = window else { return } + let mouseLocation = NSEvent.mouseLocation + let windowFrameInScreen = window.frame + + // If the suggestion is based on history, if the mouse is inside the window's frame and + // the suggestion is selected, show the delete button + if let suggestion, suggestion.isHistoryEntry, windowFrameInScreen.contains(mouseLocation) { + setRemoveButtonHidden(!isSelected) + } else { + setRemoveButtonHidden(true) + } + } + + private func setRemoveButtonHidden(_ hidden: Bool) { + removeButton.isHidden = hidden + suffixTrailingConstraint.priority = hidden ? .required : .defaultLow } } diff --git a/DuckDuckGo/Suggestions/View/SuggestionViewController.swift b/DuckDuckGo/Suggestions/View/SuggestionViewController.swift index 7d49e13c0d..f7a6765e40 100644 --- a/DuckDuckGo/Suggestions/View/SuggestionViewController.swift +++ b/DuckDuckGo/Suggestions/View/SuggestionViewController.swift @@ -18,10 +18,11 @@ import Cocoa import Combine +import History +import Suggestions protocol SuggestionViewControllerDelegate: AnyObject { - func shouldCloseSuggestionWindow(forMouseEvent event: NSEvent) -> Bool func suggestionViewControllerDidConfirmSelection(_ suggestionViewController: SuggestionViewController) } @@ -90,6 +91,15 @@ final class SuggestionViewController: NSViewController { clearSelection() } + override func viewDidLayout() { + super.viewDidLayout() + + // Make sure the table view width equals the encapsulating scroll view + tableView.sizeToFit() + let column = tableView.tableColumns.first + column?.width = tableView.frame.width + } + private func setupTableView() { tableView.style = .plain tableView.setAccessibilityIdentifier("SuggestionViewController.tableView") @@ -105,18 +115,23 @@ final class SuggestionViewController: NSViewController { tableView.addTrackingArea(trackingArea) } - private func addEventMonitors() { - eventMonitorCancellables.removeAll() + @IBAction func confirmButtonAction(_ sender: NSButton) { + delegate?.suggestionViewControllerDidConfirmSelection(self) + closeWindow() + } - NSEvent.addLocalCancellableMonitor(forEventsMatching: [.leftMouseUp, .rightMouseUp]) { [weak self] event in - guard let self else { return event } - return self.mouseUp(with: event) - }.store(in: &eventMonitorCancellables) + @IBAction func removeButtonAction(_ sender: NSButton) { + guard let cell = sender.superview as? SuggestionTableCellView, + let suggestion = cell.suggestion else { + assertionFailure("Correct cell or url are not available") + return + } - NSEvent.addLocalCancellableMonitor(forEventsMatching: [.leftMouseDown, .rightMouseDown]) { [weak self] event in - guard let self else { return event } - return self.mouseDown(with: event) - }.store(in: &eventMonitorCancellables) + removeHistory(for: suggestion) + } + + private func addEventMonitors() { + eventMonitorCancellables.removeAll() NotificationCenter.default.publisher(for: NSApplication.didResignActiveNotification).sink { [weak self] _ in self?.closeWindow() @@ -140,6 +155,10 @@ final class SuggestionViewController: NSViewController { } private func displayNewSuggestions() { + defer { + selectedRowCache = nil + } + guard suggestionContainerViewModel.numberOfSuggestions > 0 else { closeWindow() tableView.reloadData() @@ -150,12 +169,24 @@ final class SuggestionViewController: NSViewController { if suggestionContainerViewModel.suggestionContainer.result != nil { updateHeight() tableView.reloadData() + + // Select at the same position where the suggestion was removed + if let selectedRowCache = selectedRowCache { + suggestionContainerViewModel.select(at: selectedRowCache) + } + self.selectRow(at: self.suggestionContainerViewModel.selectionIndex) } } private func selectRow(at index: Int?) { - if tableView.selectedRow == index { return } + if tableView.selectedRow == index { + if let index, let cell = tableView.view(atColumn: 0, row: index, makeIfNecessary: false) as? SuggestionTableCellView { + // Show the delete button if necessary + cell.updateDeleteImageViewVisibility() + } + return + } guard let index = index, index >= 0, @@ -186,28 +217,6 @@ final class SuggestionViewController: NSViewController { clearSelection() } - func mouseDown(with event: NSEvent) -> NSEvent? { - if event.window === view.window { - return nil - } - if delegate?.shouldCloseSuggestionWindow(forMouseEvent: event) ?? true { - closeWindow() - } - - return event - } - - func mouseUp(with event: NSEvent) -> NSEvent? { - if event.window === view.window, - tableView.isMouseLocationInsideBounds(event.locationInWindow) { - - delegate?.suggestionViewControllerDidConfirmSelection(self) - closeWindow() - return nil - } - return event - } - private func updateHeight() { guard suggestionContainerViewModel.numberOfSuggestions > 0 else { tableViewHeightConstraint.constant = 0 @@ -230,6 +239,32 @@ final class SuggestionViewController: NSViewController { window.orderOut(nil) } + var selectedRowCache: Int? + + private func removeHistory(for suggestion: Suggestion) { + assert(suggestion.isHistoryEntry) + + guard let url = suggestion.url else { + assertionFailure("URL not available") + return + } + + selectedRowCache = tableView.selectedRow + + HistoryCoordinator.shared.removeUrlEntry(url) { [weak self] error in + guard let self = self, error == nil else { + return + } + + if let userStringValue = suggestionContainerViewModel.userStringValue { + suggestionContainerViewModel.isTopSuggestionSelectionExpected = false + self.suggestionContainerViewModel.suggestionContainer.getSuggestions(for: userStringValue, useCachedData: true) + } else { + self.suggestionContainerViewModel.removeSuggestionFromResult(suggestion: suggestion) + } + } + } + } extension SuggestionViewController: NSTableViewDataSource { diff --git a/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift b/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift index 37dc1bd40d..2324502656 100644 --- a/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift +++ b/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift @@ -49,7 +49,7 @@ final class SuggestionContainerViewModel { private(set) var userStringValue: String? - private var isTopSuggestionSelectionExpected = false + var isTopSuggestionSelectionExpected = false private enum IgnoreTopSuggestionError: Error { case emptyResult @@ -181,4 +181,19 @@ final class SuggestionContainerViewModel { select(at: newIndex) } + func removeSuggestionFromResult(suggestion: Suggestion) { + let topHits = suggestionContainer.result?.topHits.filter({ + !($0 == suggestion && $0.isHistoryEntry) + }) ?? [] + let duckduckgoSuggestions = suggestionContainer.result?.duckduckgoSuggestions ?? [] + let localSuggestions = suggestionContainer.result?.localSuggestions.filter({ + !($0 == suggestion && $0.isHistoryEntry) + }) ?? [] + let result = SuggestionResult(topHits: topHits, + duckduckgoSuggestions: duckduckgoSuggestions, + localSuggestions: localSuggestions) + + suggestionContainer.result = result + } + } diff --git a/DuckDuckGo/Sync/SyncDiagnosisHelper.swift b/DuckDuckGo/Sync/SyncDiagnosisHelper.swift new file mode 100644 index 0000000000..dcce535fe3 --- /dev/null +++ b/DuckDuckGo/Sync/SyncDiagnosisHelper.swift @@ -0,0 +1,63 @@ +// +// SyncDiagnosisHelper.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 DDGSync +import PixelKit + +struct SyncDiagnosisHelper { + private let userDefaults = UserDefaults.standard + private let syncService: DDGSyncing + + @UserDefaultsWrapper(key: .syncManuallyDisabledKey) + private var syncManuallyDisabled: Bool? + + @UserDefaultsWrapper(key: .syncWasDisabledUnexpectedlyPixelFiredKey, defaultValue: false) + private var syncWasDisabledUnexpectedlyPixelFired: Bool + + init(syncService: DDGSyncing) { + self.syncService = syncService + } + +// Non-user-initiated deactivation +// For events to help understand the impact of https://app.asana.com/0/1201493110486074/1208538487332133/f + + func didManuallyDisableSync() { + syncManuallyDisabled = true + } + + func diagnoseAccountStatus() { + if syncService.account == nil { + // Nil value means sync was never on in the first place. So don't fire in this case. + if syncManuallyDisabled == false, + !syncWasDisabledUnexpectedlyPixelFired { + PixelKit.fire(DebugEvent(GeneralPixel.syncDebugWasDisabledUnexpectedly), frequency: .dailyAndCount) + syncWasDisabledUnexpectedlyPixelFired = true + } + } else { + syncManuallyDisabled = false + syncWasDisabledUnexpectedlyPixelFired = false + } + } + +} + +extension UserDefaultsWrapper.DefaultsKey { + static let syncManuallyDisabledKey = Self(rawValue: "com.duckduckgo.app.key.debug.SyncManuallyDisabled") + static let syncWasDisabledUnexpectedlyPixelFiredKey = Self(rawValue: "com.duckduckgo.app.key.debug.SyncWasDisabledUnexpectedlyPixelFired") +} diff --git a/DuckDuckGo/Tab/Model/SystemInfo.swift b/DuckDuckGo/Tab/Model/SystemInfo.swift new file mode 100644 index 0000000000..58240a11e5 --- /dev/null +++ b/DuckDuckGo/Tab/Model/SystemInfo.swift @@ -0,0 +1,76 @@ +// +// SystemInfo.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 Common + +#if !APPSTORE + +final class SystemInfo { + + static func pixelParameters(appVersion: AppVersion = AppVersion.shared) async -> [String: String] { + let availableMemoryPercent = Self.getAvailableMemoryPercent() + let availableDiskSpacePercent = Self.getAvailableDiskSpacePercent() + return [ + "available_memory": String(availableMemoryPercent), + "available_diskspace": String(format: "%.2f", availableDiskSpacePercent), + "os_version": appVersion.osVersion, + ] + } + + static func getAvailableMemoryPercent() -> Int { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/memory_pressure") + + let pipe = Pipe() + process.standardOutput = pipe + + do { + try process.run() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + if let output = String(data: data, encoding: .utf8) { + // Should be the last line, but just in case search for it explicitly + let lines = output.split(separator: "\n") + if let memoryLine = lines.first(where: { $0.contains("System-wide memory free percentage") }), + let range = memoryLine.range(of: "\\d+", options: .regularExpression), + let percentage = Int(memoryLine[range]) { + return percentage + } + } + } catch { + assertionFailure("Unable to run memory_pressure") + } + + return -1 + } + + static func getAvailableDiskSpacePercent() -> Double { + guard let attributes = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()), + let totalSpace = attributes[.systemSize] as? UInt64, + let freeSpace = attributes[.systemFreeSize] as? UInt64 else { + return -1.0 + } + + return Double(freeSpace) / Double(totalSpace) * 100 + } + +} + +#endif diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index af5691c907..e4449ac5bb 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -1274,7 +1274,15 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift loadErrorHTML(error, header: UserText.webProcessCrashPageHeader, forUnreachableURL: url, alternate: true) } - PixelKit.fire(DebugEvent(GeneralPixel.webKitDidTerminate, error: error)) + Task { +#if APPSTORE + let additionalParameters = [String: String]() +#else + let additionalParameters = await SystemInfo.pixelParameters() +#endif + + PixelKit.fire(DebugEvent(GeneralPixel.webKitDidTerminate, error: error), withAdditionalParameters: additionalParameters) + } } @MainActor diff --git a/DuckDuckGo/Tab/Services/WebsiteDataStore.swift b/DuckDuckGo/Tab/Services/WebsiteDataStore.swift index 69504c873e..e4c167a284 100644 --- a/DuckDuckGo/Tab/Services/WebsiteDataStore.swift +++ b/DuckDuckGo/Tab/Services/WebsiteDataStore.swift @@ -19,6 +19,7 @@ import Common import WebKit import GRDB +import Subscription import os.log public protocol HTTPCookieStore { @@ -154,7 +155,7 @@ internal class WebCacheManager { // Don't clear fireproof domains let cookiesToRemove = cookies.filter { cookie in - !self.fireproofDomains.isFireproof(cookieDomain: cookie.domain) && cookie.domain != URL.cookieDomain + !self.fireproofDomains.isFireproof(cookieDomain: cookie.domain) && ![URL.cookieDomain, SubscriptionCookieManager.cookieDomain].contains(cookie.domain) } for cookie in cookiesToRemove { diff --git a/DuckDuckGo/Updates/AppRestarter.swift b/DuckDuckGo/Updates/AppRestarter.swift deleted file mode 100644 index fa498016f7..0000000000 --- a/DuckDuckGo/Updates/AppRestarter.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// AppRestarter.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 - -protocol AppRestarting { - - func restart() - -} - -final class AppRestarter: AppRestarting { - - func restart() { - let pid = ProcessInfo.processInfo.processIdentifier - let destinationPath = Bundle.main.bundlePath - - guard isValidApplicationBundle(at: destinationPath) else { - print("Invalid destination path") - return - } - - let preOpenCmd = "/usr/bin/xattr -d -r com.apple.quarantine \(shellQuotedString(destinationPath))" - let openCmd = "/usr/bin/open \(shellQuotedString(destinationPath))" - - let script = """ - (while /bin/kill -0 \(pid) >&/dev/null; do /bin/sleep 0.1; done; \(preOpenCmd); \(openCmd)) & - """ - - let task = Process() - task.launchPath = "/bin/sh" - task.arguments = ["-c", script] - - do { - try task.run() - } catch { - print("Unable to launch the task: \(error)") - return - } - - // Terminate the current app instance - exit(0) - } - - private func isValidApplicationBundle(at path: String) -> Bool { - let fileManager = FileManager.default - var isDirectory: ObjCBool = false - let exists = fileManager.fileExists(atPath: path, isDirectory: &isDirectory) - let isAppBundle = path.hasSuffix(".app") && isDirectory.boolValue - return exists && isAppBundle - } - - private func shellQuotedString(_ string: String) -> String { - // Validate that the string is a valid file path - guard isValidFilePath(string) else { - fatalError("Invalid file path") - } - let escapedString = string.replacingOccurrences(of: "'", with: "'\\''") - return "'\(escapedString)'" - } - - private func isValidFilePath(_ path: String) -> Bool { - // Perform validation to ensure the path is a valid and safe file path - let fileManager = FileManager.default - return fileManager.fileExists(atPath: path) - } - -} diff --git a/DuckDuckGo/Updates/BinaryOwnershipChecker.swift b/DuckDuckGo/Updates/BinaryOwnershipChecker.swift deleted file mode 100644 index e3825f92ae..0000000000 --- a/DuckDuckGo/Updates/BinaryOwnershipChecker.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// BinaryOwnershipChecker.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 Common -import os.log - -protocol BinaryOwnershipChecking { - func isCurrentUserOwner() -> Bool -} - -/// A class responsible for checking whether the current user owns the binary of the app. -/// The result is cached after the first check to avoid repeated file system access. -final class BinaryOwnershipChecker: BinaryOwnershipChecking { - - private let fileManager: FileManager - private var ownershipCache: Bool? - - init(fileManager: FileManager = .default) { - self.fileManager = fileManager - } - - /// Checks if the current user owns the binary of the currently running app. - /// The method caches the result after the first check to improve performance on subsequent calls. - /// - Returns: `true` if the current user is the owner, `false` otherwise. - func isCurrentUserOwner() -> Bool { - if let cachedResult = ownershipCache { - return cachedResult - } - - guard let binaryPath = Bundle.main.executablePath else { - Logger.updates.debug("Failed to get the binary path") - ownershipCache = false - return false - } - - do { - let attributes = try fileManager.attributesOfItem(atPath: binaryPath) - if let ownerID = attributes[FileAttributeKey.ownerAccountID] as? NSNumber { - let isOwner = ownerID.intValue == getuid() - ownershipCache = isOwner - return isOwner - } - } catch { - Logger.updates.error("Failed to get binary file attributes: \(error.localizedDescription, privacy: .public)") - } - - ownershipCache = false - return false - } -} diff --git a/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift b/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift index 9ad7e59e48..76e06dc537 100644 --- a/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift +++ b/DuckDuckGo/Updates/ReleaseNotesTabExtension.swift @@ -32,6 +32,15 @@ protocol ReleaseNotesUserScriptProvider { extension UserScripts: ReleaseNotesUserScriptProvider {} public struct ReleaseNotesValues: Codable { + enum Status: String { + case loaded + case loading + case updateReady + case updateDownloading + case updatePreparing + case updateError + case criticalUpdateReady + } let status: String let currentVersion: String @@ -40,7 +49,8 @@ public struct ReleaseNotesValues: Codable { let releaseTitle: String? let releaseNotes: [String]? let releaseNotesPrivacyPro: [String]? - + let downloadProgress: Double? + let automaticUpdate: Bool? } final class ReleaseNotesTabExtension: NavigationResponder { @@ -84,7 +94,7 @@ final class ReleaseNotesTabExtension: NavigationResponder { return } let updateController = Application.appDelegate.updateController! - Publishers.CombineLatest(updateController.isUpdateBeingLoadedPublisher, updateController.latestUpdatePublisher) + Publishers.CombineLatest(updateController.updateProgressPublisher, updateController.latestUpdatePublisher) .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self else { return } @@ -93,6 +103,14 @@ final class ReleaseNotesTabExtension: NavigationResponder { .store(in: &cancellables) } + @MainActor + func navigationDidFinish(_ navigation: Navigation) { +#if !DEBUG + guard NSApp.runType != .uiTests, navigation.url.isReleaseNotesScheme else { return } + let updateController = Application.appDelegate.updateController! + updateController.checkForUpdateIfNeeded() +#endif + } } protocol ReleaseNotesTabExtensionProtocol: AnyObject, NavigationResponder {} @@ -107,49 +125,87 @@ extension TabExtensions { extension ReleaseNotesValues { - init(status: String, + init(status: Status, currentVersion: String, - lastUpdate: UInt) { - self.init(status: status, - currentVersion: currentVersion, - latestVersion: nil, - lastUpdate: lastUpdate, - releaseTitle: nil, - releaseNotes: nil, - releaseNotesPrivacyPro: nil) + latestVersion: String? = nil, + lastUpdate: UInt, + releaseTitle: String? = nil, + releaseNotes: [String]? = nil, + releaseNotesPrivacyPro: [String]? = nil, + downloadProgress: Double? = nil, + automaticUpdate: Bool? = nil) { + self.status = status.rawValue + self.currentVersion = currentVersion + self.latestVersion = latestVersion + self.lastUpdate = lastUpdate + self.releaseTitle = releaseTitle + self.releaseNotes = releaseNotes + self.releaseNotesPrivacyPro = releaseNotesPrivacyPro + self.downloadProgress = downloadProgress + self.automaticUpdate = automaticUpdate } init(from updateController: UpdateController?) { let currentVersion = "\(AppVersion().versionNumber) (\(AppVersion().buildNumber))" let lastUpdate = UInt((updateController?.lastUpdateCheckDate ?? Date()).timeIntervalSince1970) - let status: String - let latestVersion: String - guard let updateController, !updateController.isUpdateBeingLoaded else { - self.init(status: "loading", + guard let updateController, let latestUpdate = updateController.latestUpdate else { + self.init(status: updateController?.updateProgress.toStatus ?? .loaded, currentVersion: currentVersion, lastUpdate: lastUpdate) return } - if let latestUpdate = updateController.latestUpdate { - status = latestUpdate.isInstalled ? "loaded" : "updateReady" - latestVersion = "\(latestUpdate.version) (\(latestUpdate.build))" - self.init(status: status, - currentVersion: currentVersion, - latestVersion: latestVersion, - lastUpdate: lastUpdate, - releaseTitle: latestUpdate.title, - releaseNotes: latestUpdate.releaseNotes, - releaseNotesPrivacyPro: latestUpdate.releaseNotesPrivacyPro) - return - } else { - self.init(status: "loaded", - currentVersion: currentVersion, - lastUpdate: lastUpdate) + let updateState = UpdateState(from: updateController.latestUpdate, progress: updateController.updateProgress) + + let status: Status + let downloadProgress: Double? + switch updateState { + case .upToDate: + status = .loaded + downloadProgress = nil + case .updateCycle(let progress): + if updateController.hasPendingUpdate { + status = updateController.latestUpdate?.type == .critical ? .criticalUpdateReady : .updateReady + } else { + status = progress.toStatus + } + downloadProgress = progress.toDownloadProgress } + + self.init(status: status, + currentVersion: currentVersion, + latestVersion: latestUpdate.versionString, + lastUpdate: lastUpdate, + releaseTitle: latestUpdate.title, + releaseNotes: latestUpdate.releaseNotes, + releaseNotesPrivacyPro: latestUpdate.releaseNotesPrivacyPro, + downloadProgress: downloadProgress, + automaticUpdate: updateController.areAutomaticUpdatesEnabled) } +} +private extension Update { + var versionString: String? { + "\(version) \(build)" + } +} + +private extension UpdateCycleProgress { + var toStatus: ReleaseNotesValues.Status { + switch self { + case .updateCycleDidStart: return .loading + case .downloadDidStart, .downloading: return .updateDownloading + case .extractionDidStart, .extracting, .readyToInstallAndRelaunch, .installationDidStart, .installing: return .updatePreparing + case .updaterError: return .updateError + case .updateCycleNotStarted, .updateCycleDone: return .updateReady + } + } + + var toDownloadProgress: Double? { + guard case .downloading(let percentage) = self else { return nil } + return percentage + } } #else diff --git a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift index 199d0da2d0..620c2ff3b9 100644 --- a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift +++ b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift @@ -43,6 +43,7 @@ final class ReleaseNotesUserScript: NSObject, Subfeature { case reportPageException case reportInitException case browserRestart + case retryUpdate } override init() { @@ -57,7 +58,8 @@ final class ReleaseNotesUserScript: NSObject, Subfeature { .initialSetup: initialSetup, .reportPageException: reportPageException, .reportInitException: reportInitException, - .browserRestart: browserRestart + .browserRestart: browserRestart, + .retryUpdate: retryUpdate, ] @MainActor @@ -108,6 +110,14 @@ extension ReleaseNotesUserScript { return InitialSetupResult(env: env, locale: Locale.current.identifier) } + @MainActor + private func retryUpdate(params: Any, original: WKScriptMessage) async throws -> Encodable? { + DispatchQueue.main.async { [weak self] in + self?.updateController.checkForUpdateIfNeeded() + } + return nil + } + struct InitialSetupResult: Encodable { let env: String let locale: String diff --git a/DuckDuckGo/Updates/UpdateController.swift b/DuckDuckGo/Updates/UpdateController.swift index 2d7b4ffec8..f044522f63 100644 --- a/DuckDuckGo/Updates/UpdateController.swift +++ b/DuckDuckGo/Updates/UpdateController.swift @@ -31,17 +31,18 @@ protocol UpdateControllerProtocol: AnyObject { var latestUpdate: Update? { get } var latestUpdatePublisher: Published.Publisher { get } - var isUpdateAvailableToInstall: Bool { get } - var isUpdateAvailableToInstallPublisher: Published.Publisher { get } + var hasPendingUpdate: Bool { get } + var hasPendingUpdatePublisher: Published.Publisher { get } - var isUpdateBeingLoaded: Bool { get } - var isUpdateBeingLoadedPublisher: Published.Publisher { get } + var needsNotificationDot: Bool { get set } + var notificationDotPublisher: AnyPublisher { get } - var lastUpdateCheckDate: Date? { get } + var updateProgress: UpdateCycleProgress { get } + var updateProgressPublisher: Published.Publisher { get } - func checkForUpdate() - func checkForUpdateInBackground() + var lastUpdateCheckDate: Date? { get } + func checkForUpdateIfNeeded() func runUpdate() var areAutomaticUpdatesEnabled: Bool { get set } @@ -59,85 +60,75 @@ final class UpdateController: NSObject, UpdateControllerProtocol { lazy var notificationPresenter = UpdateNotificationPresenter() let willRelaunchAppPublisher: AnyPublisher - @Published private(set) var isUpdateBeingLoaded = false - var isUpdateBeingLoadedPublisher: Published.Publisher { $isUpdateBeingLoaded } - // Struct used to cache data until the updater finishes checking for updates struct UpdateCheckResult { let item: SUAppcastItem let isInstalled: Bool } - private var updateCheckResult: UpdateCheckResult? + private var cachedUpdateResult: UpdateCheckResult? - @Published private(set) var latestUpdate: Update? { + @Published private(set) var updateProgress = UpdateCycleProgress.default { didSet { - if let latestUpdate, !latestUpdate.isInstalled { - if !shouldShowManualUpdateDialog { - switch latestUpdate.type { - case .critical: - notificationPresenter.showUpdateNotification(icon: NSImage.criticalUpdateNotificationInfo, text: UserText.criticalUpdateNotification, presentMultiline: true) - case .regular: - notificationPresenter.showUpdateNotification(icon: NSImage.updateNotificationInfo, text: UserText.updateAvailableNotification, presentMultiline: true) - } - } - isUpdateAvailableToInstall = !latestUpdate.isInstalled - } else { - isUpdateAvailableToInstall = false + if let cachedUpdateResult { + latestUpdate = Update(appcastItem: cachedUpdateResult.item, isInstalled: cachedUpdateResult.isInstalled) + hasPendingUpdate = latestUpdate?.isInstalled == false && updateProgress.isIdle + needsNotificationDot = hasPendingUpdate } + showUpdateNotificationIfNeeded() } } + var updateProgressPublisher: Published.Publisher { $updateProgress } + + @Published private(set) var latestUpdate: Update? + var latestUpdatePublisher: Published.Publisher { $latestUpdate } - @Published private(set) var isUpdateAvailableToInstall = false - var isUpdateAvailableToInstallPublisher: Published.Publisher { $isUpdateAvailableToInstall } + @Published private(set) var hasPendingUpdate = false + var hasPendingUpdatePublisher: Published.Publisher { $hasPendingUpdate } + + var lastUpdateCheckDate: Date? { updater?.lastUpdateCheckDate } + var lastUpdateNotificationShownDate: Date = .distantPast - var lastUpdateCheckDate: Date? { - updater.updater.lastUpdateCheckDate + private var shouldShowUpdateNotification: Bool { + Date().timeIntervalSince(lastUpdateNotificationShownDate) > .days(7) } @UserDefaultsWrapper(key: .automaticUpdates, defaultValue: true) var areAutomaticUpdatesEnabled: Bool { didSet { - Logger.updates.debug("areAutomaticUpdatesEnabled: \(self.areAutomaticUpdatesEnabled)") - if updater.updater.automaticallyDownloadsUpdates != areAutomaticUpdatesEnabled { - updater.updater.automaticallyDownloadsUpdates = areAutomaticUpdatesEnabled - - // Reinitialize in order to reset the current loaded state - if !areAutomaticUpdatesEnabled { - configureUpdater() - latestUpdate = nil - } + Logger.updates.log("areAutomaticUpdatesEnabled: \(self.areAutomaticUpdatesEnabled)") + if oldValue != areAutomaticUpdatesEnabled { + userDriver?.cancelAndDismissCurrentUpdate() + try? configureUpdater() } } } - var automaticUpdateFlow: Bool { - // In case the current user is not the owner of the binary, we have to switch - // to manual update flow because the authentication is required. - return areAutomaticUpdatesEnabled && binaryOwnershipChecker.isCurrentUserOwner() + @UserDefaultsWrapper(key: .pendingUpdateShown, defaultValue: false) + var needsNotificationDot: Bool { + didSet { + notificationDotSubject.send(needsNotificationDot) + } } - var shouldShowManualUpdateDialog = false + private let notificationDotSubject = CurrentValueSubject(false) + lazy var notificationDotPublisher = notificationDotSubject.eraseToAnyPublisher() - private(set) var updater: SPUStandardUpdaterController! - private var appRestarter: AppRestarting + private(set) var updater: SPUUpdater? + private(set) var userDriver: UpdateUserDriver? private let willRelaunchAppSubject = PassthroughSubject() private var internalUserDecider: InternalUserDecider - private let binaryOwnershipChecker: BinaryOwnershipChecking + private var updateProcessCancellable: AnyCancellable! // MARK: - Public - init(internalUserDecider: InternalUserDecider, - appRestarter: AppRestarting = AppRestarter(), - binaryOwnershipChecker: BinaryOwnershipChecking = BinaryOwnershipChecker()) { + init(internalUserDecider: InternalUserDecider) { willRelaunchAppPublisher = willRelaunchAppSubject.eraseToAnyPublisher() self.internalUserDecider = internalUserDecider - self.appRestarter = appRestarter - self.binaryOwnershipChecker = binaryOwnershipChecker super.init() - configureUpdater() + try? configureUpdater() } func checkNewApplicationVersion() { @@ -151,80 +142,80 @@ final class UpdateController: NSObject, UpdateControllerProtocol { } } - func checkForUpdate() { - Logger.updates.debug("Checking for updates") - - updater.updater.checkForUpdates() - } - - func checkForUpdateInBackground() { - Logger.updates.debug("Checking for updates in background") - - updater.updater.checkForUpdatesInBackground() - } + func checkForUpdateIfNeeded() { + guard let updater, !updater.sessionInProgress else { return } - @objc func runUpdate() { - PixelKit.fire(DebugEvent(GeneralPixel.updaterDidRunUpdate)) + Logger.updates.log("Checking for updates") - if automaticUpdateFlow { - appRestarter.restart() - } else { - updater.userDriver.activeUpdateAlert?.hideUnnecessaryUpdateButtons() - shouldShowManualUpdateDialog = true - checkForUpdate() - } + updater.checkForUpdates() } // MARK: - Private - private func configureUpdater() { + private func configureUpdater() throws { + // Workaround to reset the updater state + cachedUpdateResult = nil + latestUpdate = nil + // The default configuration of Sparkle updates is in Info.plist - updater = SPUStandardUpdaterController(updaterDelegate: self, userDriverDelegate: self) - shouldShowManualUpdateDialog = false + userDriver = UpdateUserDriver(internalUserDecider: internalUserDecider, + areAutomaticUpdatesEnabled: areAutomaticUpdatesEnabled) + guard let userDriver else { return } - if updater.updater.automaticallyDownloadsUpdates != automaticUpdateFlow { - updater.updater.automaticallyDownloadsUpdates = automaticUpdateFlow - } + updater = SPUUpdater(hostBundle: Bundle.main, applicationBundle: Bundle.main, userDriver: userDriver, delegate: self) + + updateProcessCancellable = userDriver.updateProgressPublisher + .assign(to: \.updateProgress, onWeaklyHeld: self) + + try updater?.start() #if DEBUG - updater.updater.automaticallyChecksForUpdates = false - updater.updater.automaticallyDownloadsUpdates = false - updater.updater.updateCheckInterval = 0 + updater?.automaticallyChecksForUpdates = false + updater?.automaticallyDownloadsUpdates = false + updater?.updateCheckInterval = 0 #else - // Load the appcast to retrieve information about the latest update (required for displaying Release Notes) - checkForUpdateInBackground() + checkForUpdateIfNeeded() #endif } - @objc private func openUpdatesPage() { - notificationPresenter.openUpdatesPage() - } - -} + private func showUpdateNotificationIfNeeded() { + guard let latestUpdate, hasPendingUpdate, shouldShowUpdateNotification else { return } + + let action = areAutomaticUpdatesEnabled ? UserText.autoUpdateAction : UserText.manualUpdateAction + + switch latestUpdate.type { + case .critical: + notificationPresenter.showUpdateNotification( + icon: NSImage.criticalUpdateNotificationInfo, + text: "\(UserText.criticalUpdateNotification) \(action)", + presentMultiline: true + ) + case .regular: + notificationPresenter.showUpdateNotification( + icon: NSImage.updateNotificationInfo, + text: "\(UserText.updateAvailableNotification) \(action)", + presentMultiline: true + ) + } -extension UpdateController: SPUStandardUserDriverDelegate { + lastUpdateNotificationShownDate = Date() + } - func standardUserDriverShouldHandleShowingScheduledUpdate(_ update: SUAppcastItem, andInImmediateFocus immediateFocus: Bool) -> Bool { - return shouldShowManualUpdateDialog + @objc func openUpdatesPage() { + notificationPresenter.openUpdatesPage() } - func standardUserDriverWillHandleShowingUpdate(_ handleShowingUpdate: Bool, forUpdate update: SUAppcastItem, state: SPUUserUpdateState) {} + @objc func runUpdate() { + if let userDriver { + PixelKit.fire(DebugEvent(GeneralPixel.updaterDidRunUpdate)) + userDriver.resume() + } + } } extension UpdateController: SPUUpdaterDelegate { - func updater(_ updater: SPUUpdater, mayPerform updateCheck: SPUUpdateCheck) throws { - Logger.updates.debug("Updater started performing the update check. (isInternalUser: \(self.internalUserDecider.isInternalUser)") - - onUpdateCheckStart() - } - - private func onUpdateCheckStart() { - updateCheckResult = nil - isUpdateBeingLoaded = true - } - func allowedChannels(for updater: SPUUpdater) -> Set { if internalUserDecider.isInternalUser { return Set([Constants.internalChannelName]) @@ -251,63 +242,41 @@ extension UpdateController: SPUUpdaterDelegate { } func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) { - Logger.updates.debug("Updater did find valid update: \(item.displayVersionString)(\(item.versionString))") - + Logger.updates.log("Updater did find valid update: \(item.displayVersionString)(\(item.versionString))") PixelKit.fire(DebugEvent(GeneralPixel.updaterDidFindUpdate)) - - if !automaticUpdateFlow { - // For manual updates, we can present the available update without waiting for the update cycle to finish. The Sparkle flow downloads the update later - updateCheckResult = UpdateCheckResult(item: item, isInstalled: false) - onUpdateCheckEnd() - } + cachedUpdateResult = UpdateCheckResult(item: item, isInstalled: false) } func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: any Error) { - let item = (error as NSError).userInfo["SULatestAppcastItemFound"] as? SUAppcastItem - Logger.updates.debug("Updater did not find update: \(String(describing: item?.displayVersionString))(\(String(describing: item?.versionString)))") - if let item { - // User is running the latest version - updateCheckResult = UpdateCheckResult(item: item, isInstalled: true) - } + let nsError = error as NSError + guard let item = nsError.userInfo["SULatestAppcastItemFound"] as? SUAppcastItem else { return } + Logger.updates.log("Updater did not find update: \(String(describing: item.displayVersionString))(\(String(describing: item.versionString)))") PixelKit.fire(DebugEvent(GeneralPixel.updaterDidNotFindUpdate, error: error)) + + cachedUpdateResult = UpdateCheckResult(item: item, isInstalled: true) } func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) { - Logger.updates.debug("Updater did download update: \(item.displayVersionString)(\(item.versionString))") - - if automaticUpdateFlow { - // For automatic updates, the available item has to be downloaded - updateCheckResult = UpdateCheckResult(item: item, isInstalled: false) - return - } - + Logger.updates.log("Updater did download update: \(item.displayVersionString)(\(item.versionString))") PixelKit.fire(DebugEvent(GeneralPixel.updaterDidDownloadUpdate)) } - func updater(_ updater: SPUUpdater, didFinishUpdateCycleFor updateCheck: SPUUpdateCheck, error: (any Error)?) { - Logger.updates.debug("Updater did finish update cycle") - - onUpdateCheckEnd() + func updater(_ updater: SPUUpdater, didExtractUpdate item: SUAppcastItem) { + Logger.updates.log("Updater did extract update: \(item.displayVersionString)(\(item.versionString))") } - private func onUpdateCheckEnd() { - guard isUpdateBeingLoaded else { - // The update check end is already handled - return - } + func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) { + Logger.updates.log("Updater will install update: \(item.displayVersionString)(\(item.versionString))") + } - // If the update is available, present it - if let updateCheckResult = updateCheckResult { - latestUpdate = Update(appcastItem: updateCheckResult.item, - isInstalled: updateCheckResult.isInstalled) + func updater(_ updater: SPUUpdater, didFinishUpdateCycleFor updateCheck: SPUUpdateCheck, error: (any Error)?) { + if error == nil { + Logger.updates.log("Updater did finish update cycle") + updateProgress = .updateCycleDone } else { - latestUpdate = nil + Logger.updates.log("Updater did finish update cycle with error") } - - // Clear cache - isUpdateBeingLoaded = false - updateCheckResult = nil } } diff --git a/DuckDuckGo/Updates/UpdateNotificationPresenter.swift b/DuckDuckGo/Updates/UpdateNotificationPresenter.swift index 5c68f0c64b..bea8259c16 100644 --- a/DuckDuckGo/Updates/UpdateNotificationPresenter.swift +++ b/DuckDuckGo/Updates/UpdateNotificationPresenter.swift @@ -26,7 +26,7 @@ final class UpdateNotificationPresenter { static let presentationTimeInterval: TimeInterval = 10 func showUpdateNotification(icon: NSImage, text: String, buttonText: String? = nil, presentMultiline: Bool = false) { - Logger.updates.debug("Notification presented: \(text)") + Logger.updates.log("Notification presented: \(text)") DispatchQueue.main.async { guard let windowController = WindowControllersManager.shared.lastKeyMainWindowController ?? WindowControllersManager.shared.mainWindowControllers.last, @@ -34,6 +34,12 @@ final class UpdateNotificationPresenter { return } + let parentViewController = windowController.mainViewController + + guard parentViewController.view.window?.isKeyWindow == true, (parentViewController.presentedViewControllers ?? []).isEmpty else { + return + } + let buttonAction: (() -> Void)? = { [weak self] in self?.openUpdatesPage() } @@ -49,8 +55,7 @@ final class UpdateNotificationPresenter { self?.openUpdatesPage() }) - viewController.show(onParent: windowController.mainViewController, - relativeTo: button) + viewController.show(onParent: parentViewController, relativeTo: button) } } diff --git a/DuckDuckGo/Updates/UpdateUserDriver.swift b/DuckDuckGo/Updates/UpdateUserDriver.swift new file mode 100644 index 0000000000..960825f46e --- /dev/null +++ b/DuckDuckGo/Updates/UpdateUserDriver.swift @@ -0,0 +1,214 @@ +// +// UpdateUserDriver.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 Sparkle +import PixelKit +import BrowserServicesKit +import Combine +import os.log + +#if SPARKLE + +enum UpdateState { + case upToDate + case updateCycle(UpdateCycleProgress) + + init(from update: Update?, progress: UpdateCycleProgress) { + if let update, !update.isInstalled { + self = .updateCycle(progress) + } else if progress.isFailed { + self = .updateCycle(progress) + } else { + self = .upToDate + } + } +} + +enum UpdateCycleProgress { + case updateCycleNotStarted + case updateCycleDidStart + case updateCycleDone + case downloadDidStart + case downloading(Double) + case extractionDidStart + case extracting(Double) + case readyToInstallAndRelaunch + case installationDidStart + case installing + case updaterError(Error) + + static var `default` = UpdateCycleProgress.updateCycleNotStarted + + var isDone: Bool { + switch self { + case .updateCycleDone: return true + default: return false + } + } + + var isIdle: Bool { + switch self { + case .updateCycleDone, .updateCycleNotStarted, .updaterError: return true + default: return false + } + } + + var isFailed: Bool { + switch self { + case .updaterError: return true + default: return false + } + } +} + +final class UpdateUserDriver: NSObject, SPUUserDriver { + enum Checkpoint: Equatable { + case download + case restart + } + + private var internalUserDecider: InternalUserDecider + + private var checkpoint: Checkpoint + private var onResuming: () -> Void = {} + + private var onSkipping: () -> Void = {} + + private var bytesToDownload: UInt64 = 0 + private var bytesDownloaded: UInt64 = 0 + + @Published var updateProgress = UpdateCycleProgress.default + public var updateProgressPublisher: Published.Publisher { $updateProgress } + + init(internalUserDecider: InternalUserDecider, + areAutomaticUpdatesEnabled: Bool) { + self.internalUserDecider = internalUserDecider + self.checkpoint = areAutomaticUpdatesEnabled ? .restart : .download + } + + func resume() { + onResuming() + } + + func cancelAndDismissCurrentUpdate() { + onSkipping() + } + + func show(_ request: SPUUpdatePermissionRequest) async -> SUUpdatePermissionResponse { +#if DEBUG + .init(automaticUpdateChecks: false, sendSystemProfile: false) +#else + .init(automaticUpdateChecks: true, sendSystemProfile: false) +#endif + } + + func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { + Logger.updates.log("Updater started performing the update check. (isInternalUser: \(self.internalUserDecider.isInternalUser)") + updateProgress = .updateCycleDidStart + } + + func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping (SPUUserUpdateChoice) -> Void) { + if appcastItem.isInformationOnlyUpdate { + reply(.dismiss) + } + + onSkipping = { reply(.skip) } + + if checkpoint == .download { + onResuming = { reply(.install) } + updateProgress = .updateCycleDone + } else { + reply(.install) + } + } + + func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { + // no-op + } + + func showUpdateReleaseNotesFailedToDownloadWithError(_ error: any Error) { + // no-op + } + + func showUpdateNotFoundWithError(_ error: any Error, acknowledgement: @escaping () -> Void) { + acknowledgement() + } + + func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) { + updateProgress = .updaterError(error) + acknowledgement() + } + + func showDownloadInitiated(cancellation: @escaping () -> Void) { + updateProgress = .downloadDidStart + } + + func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { + bytesDownloaded = 0 + bytesToDownload = expectedContentLength + } + + func showDownloadDidReceiveData(ofLength length: UInt64) { + bytesDownloaded += length + if bytesDownloaded > bytesToDownload { + bytesToDownload = bytesDownloaded + } + updateProgress = .downloading(Double(bytesDownloaded) / Double(bytesToDownload)) + } + + func showDownloadDidStartExtractingUpdate() { + updateProgress = .extractionDidStart + } + + func showExtractionReceivedProgress(_ progress: Double) { + updateProgress = .extracting(progress) + } + + func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) { + onSkipping = { reply(.skip) } + + if checkpoint == .restart { + onResuming = { reply(.install) } + } else { + reply(.install) + } + + updateProgress = .updateCycleDone + } + + func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) { + updateProgress = .installationDidStart + } + + func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { + updateProgress = .installing + acknowledgement() + } + + func showUpdateInFocus() { + // no-op + } + + func dismissUpdateInstallation() { + guard !updateProgress.isFailed else { return } + updateProgress = .updateCycleDone + } +} + +#endif diff --git a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift index f9ef9def11..8f91c6078c 100644 --- a/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift +++ b/LocalPackages/BuildToolPlugins/Plugins/InputFilesChecker/InputFilesChecker.swift @@ -21,11 +21,11 @@ import PackagePlugin import XcodeProjectPlugin let nonSandboxedExtraInputFiles: Set = [ - .init("BinaryOwnershipChecker.swift", .source), .init("BWEncryption.m", .source), .init("BWEncryptionOutput.m", .source), .init("BWManager.swift", .source), .init("UpdateController.swift", .source), + .init("UpdateUserDriver.swift", .source), .init("PFMoveApplication.m", .source), .init("DuckDuckGo VPN.app", .unknown), .init("DuckDuckGo Notifications.app", .unknown), @@ -50,7 +50,6 @@ let extraInputFiles: [TargetName: Set] = [ "DuckDuckGo Privacy Pro": nonSandboxedExtraInputFiles, "Unit Tests": [ - .init("BinaryOwnershipCheckerTests.swift", .source), .init("BWEncryptionTests.swift", .source), .init("WKWebViewPrivateMethodsAvailabilityTests.swift", .source) ], diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index deea3ccec5..81c1a4d1b9 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "201.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "203.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), .package(path: "../Freemium"), diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index ffb525774a..f6ad3673cb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -409,10 +409,10 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { // 2. We map the brokers to the UI model .flatMap { dataBroker -> [DBPUIDataBroker] in var result: [DBPUIDataBroker] = [] - result.append(DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url, parentURL: dataBroker.parent)) + result.append(DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url, parentURL: dataBroker.parent, optOutUrl: dataBroker.optOutUrl)) for mirrorSite in dataBroker.mirrorSites { - result.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, parentURL: dataBroker.parent)) + result.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, parentURL: dataBroker.parent, optOutUrl: dataBroker.optOutUrl)) } return result } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index 24fe6b6a1a..68e5f19d0e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -127,12 +127,14 @@ struct DBPUIDataBroker: Codable, Hashable { let url: String let date: Double? let parentURL: String? + let optOutUrl: String - init(name: String, url: String, date: Double? = nil, parentURL: String?) { + init(name: String, url: String, date: Double? = nil, parentURL: String?, optOutUrl: String) { self.name = name self.url = url self.date = date self.parentURL = parentURL + self.optOutUrl = optOutUrl } func hash(into hasher: inout Hasher) { @@ -170,7 +172,8 @@ extension DBPUIDataBrokerProfileMatch { dataBrokerName: String, dataBrokerURL: String, dataBrokerParentURL: String?, - parentBrokerOptOutJobData: [OptOutJobData]?) { + parentBrokerOptOutJobData: [OptOutJobData]?, + optOutUrl: String) { let extractedProfile = optOutJobData.extractedProfile /* @@ -205,7 +208,7 @@ extension DBPUIDataBrokerProfileMatch { extractedProfile.doesMatchExtractedProfile(parentOptOut.extractedProfile) } ?? false - self.init(dataBroker: DBPUIDataBroker(name: dataBrokerName, url: dataBrokerURL, parentURL: dataBrokerParentURL), + self.init(dataBroker: DBPUIDataBroker(name: dataBrokerName, url: dataBrokerURL, parentURL: dataBrokerParentURL, optOutUrl: optOutUrl), name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), @@ -217,12 +220,13 @@ extension DBPUIDataBrokerProfileMatch { hasMatchingRecordOnParentBroker: hasFoundParentMatch) } - init(optOutJobData: OptOutJobData, dataBroker: DataBroker, parentBrokerOptOutJobData: [OptOutJobData]?) { + init(optOutJobData: OptOutJobData, dataBroker: DataBroker, parentBrokerOptOutJobData: [OptOutJobData]?, optOutUrl: String) { self.init(optOutJobData: optOutJobData, dataBrokerName: dataBroker.name, dataBrokerURL: dataBroker.url, dataBrokerParentURL: dataBroker.parent, - parentBrokerOptOutJobData: parentBrokerOptOutJobData) + parentBrokerOptOutJobData: parentBrokerOptOutJobData, + optOutUrl: optOutUrl) } /// Generates an array of `DBPUIDataBrokerProfileMatch` objects from the provided query data. @@ -253,7 +257,8 @@ extension DBPUIDataBrokerProfileMatch { // Create a profile match for the current data broker and append it to the list of profiles. profiles.append(DBPUIDataBrokerProfileMatch(optOutJobData: optOutJobData, dataBroker: dataBroker, - parentBrokerOptOutJobData: parentBrokerOptOutJobData)) + parentBrokerOptOutJobData: parentBrokerOptOutJobData, + optOutUrl: dataBroker.optOutUrl)) // Handle mirror sites associated with the data broker. if !dataBroker.mirrorSites.isEmpty { @@ -264,7 +269,8 @@ extension DBPUIDataBrokerProfileMatch { dataBrokerName: mirrorSite.name, dataBrokerURL: mirrorSite.url, dataBrokerParentURL: dataBroker.parent, - parentBrokerOptOutJobData: parentBrokerOptOutJobData) + parentBrokerOptOutJobData: parentBrokerOptOutJobData, + optOutUrl: dataBroker.optOutUrl) } return nil } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift index 9014bcb185..5328a699f6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift @@ -115,6 +115,7 @@ public struct DataBroker: Codable, Sendable { let schedulingConfig: DataBrokerScheduleConfig let parent: String? let mirrorSites: [MirrorSite] + let optOutUrl: String var isFakeBroker: Bool { name.contains("fake") // A future improvement will be to add a property in the JSON file. @@ -128,6 +129,7 @@ public struct DataBroker: Codable, Sendable { case schedulingConfig case parent case mirrorSites + case optOutUrl } init(id: Int64? = nil, @@ -137,7 +139,8 @@ public struct DataBroker: Codable, Sendable { version: String, schedulingConfig: DataBrokerScheduleConfig, parent: String? = nil, - mirrorSites: [MirrorSite] = [MirrorSite]() + mirrorSites: [MirrorSite] = [MirrorSite](), + optOutUrl: String ) { self.id = id self.name = name @@ -153,6 +156,7 @@ public struct DataBroker: Codable, Sendable { self.schedulingConfig = schedulingConfig self.parent = parent self.mirrorSites = mirrorSites + self.optOutUrl = optOutUrl } public init(from decoder: Decoder) throws { @@ -179,6 +183,8 @@ public struct DataBroker: Codable, Sendable { mirrorSites = [MirrorSite]() } + optOutUrl = (try? container.decode(String.self, forKey: .optOutUrl)) ?? "" + id = nil } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift index fc221f3872..ab3be3f110 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift @@ -176,7 +176,8 @@ struct MapperToModel { version: decodedBroker.version, schedulingConfig: decodedBroker.schedulingConfig, parent: decodedBroker.parent, - mirrorSites: decodedBroker.mirrorSites + mirrorSites: decodedBroker.mirrorSites, + optOutUrl: decodedBroker.optOutUrl ) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift index 16f78bfe45..5992962044 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift @@ -79,7 +79,7 @@ struct DBPUICommunicationLayer: Subfeature { weak var delegate: DBPUICommunicationDelegate? private enum Constants { - static let version = 7 + static let version = 8 } internal init(webURLSettings: DataBrokerProtectionWebUIURLSettingsRepresentable, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index 4636f3b58d..36efa17943 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -73,7 +73,8 @@ struct MapperToUI { let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOutJob, dataBroker: dataBroker, - parentBrokerOptOutJobData: parentBrokerOptOutJobData) + parentBrokerOptOutJobData: parentBrokerOptOutJobData, + optOutUrl: dataBroker.optOutUrl) if extractedProfile.removedDate == nil { inProgressOptOuts.append(profileMatch) @@ -87,7 +88,8 @@ struct MapperToUI { dataBrokerName: mirrorSite.name, dataBrokerURL: mirrorSite.url, dataBrokerParentURL: dataBroker.parent, - parentBrokerOptOutJobData: parentBrokerOptOutJobData) + parentBrokerOptOutJobData: parentBrokerOptOutJobData, + optOutUrl: dataBroker.optOutUrl) if let extractedProfileRemovedDate = extractedProfile.removedDate, mirrorSite.shouldWeIncludeMirrorSite(for: extractedProfileRemovedDate) { @@ -135,13 +137,15 @@ struct MapperToUI { brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanJobData.lastRunDate!.timeIntervalSince1970, - parentURL: $0.dataBroker.parent)) + parentURL: $0.dataBroker.parent, + optOutUrl: $0.dataBroker.optOutUrl)) for mirrorSite in $0.dataBroker.mirrorSites where mirrorSite.addedAt < $0.scanJobData.lastRunDate! { brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.lastRunDate!.timeIntervalSince1970, - parentURL: $0.dataBroker.parent)) + parentURL: $0.dataBroker.parent, + optOutUrl: $0.dataBroker.optOutUrl)) } return brokers @@ -171,7 +175,8 @@ struct MapperToUI { brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970, - parentURL: $0.dataBroker.parent)) + parentURL: $0.dataBroker.parent, + optOutUrl: $0.dataBroker.optOutUrl)) for mirrorSite in $0.dataBroker.mirrorSites { if let removedDate = mirrorSite.removedAt { @@ -179,13 +184,15 @@ struct MapperToUI { brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970, - parentURL: $0.dataBroker.parent)) + parentURL: $0.dataBroker.parent, + optOutUrl: $0.dataBroker.optOutUrl)) } } else { brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970, - parentURL: $0.dataBroker.parent)) + parentURL: $0.dataBroker.parent, + optOutUrl: $0.dataBroker.optOutUrl)) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift index 68b71f4fdd..aff311ce82 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift @@ -46,7 +46,8 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerName: "doesn't matter for the test", dataBrokerURL: "see above", dataBrokerParentURL: "whatever", - parentBrokerOptOutJobData: nil) + parentBrokerOptOutJobData: nil, + optOutUrl: "broker.com") // Then XCTAssertEqual(profileMatch.foundDate, createdDate.timeIntervalSince1970) @@ -77,7 +78,8 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerName: "doesn't matter for the test", dataBrokerURL: "see above", dataBrokerParentURL: "whatever", - parentBrokerOptOutJobData: nil) + parentBrokerOptOutJobData: nil, + optOutUrl: "broker.com") // Then XCTAssertEqual(profileMatch.foundDate, foundEventDate.timeIntervalSince1970) @@ -116,7 +118,8 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerName: "doesn't matter for the test", dataBrokerURL: "see above", dataBrokerParentURL: "whatever", - parentBrokerOptOutJobData: nil) + parentBrokerOptOutJobData: nil, + optOutUrl: "broker.com") // Then XCTAssertEqual(profileMatch.foundDate, foundEventDate2.timeIntervalSince1970) @@ -147,7 +150,8 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerName: "doesn't matter for the test", dataBrokerURL: "see above", dataBrokerParentURL: "whatever", - parentBrokerOptOutJobData: [parentOptOut]) + parentBrokerOptOutJobData: [parentOptOut], + optOutUrl: "broker.com") // Then XCTAssertTrue(profileMatch.hasMatchingRecordOnParentBroker) @@ -177,7 +181,8 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerParentURL: "whatever", parentBrokerOptOutJobData: [parentOptOutNonmatching1, parentOptOutMatching, - parentOptOutNonmatching2]) + parentOptOutNonmatching2], + optOutUrl: "broker.com") // Then XCTAssertTrue(profileMatch.hasMatchingRecordOnParentBroker) @@ -203,7 +208,8 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerURL: "see above", dataBrokerParentURL: "whatever", parentBrokerOptOutJobData: [parentOptOutNonmatching1, - parentOptOutNonmatching2]) + parentOptOutNonmatching2], + optOutUrl: "broker.com") // Then XCTAssertFalse(profileMatch.hasMatchingRecordOnParentBroker) @@ -225,9 +231,70 @@ final class DBPUICommunicationModelTests: XCTestCase { dataBrokerName: "doesn't matter for the test", dataBrokerURL: "see above", dataBrokerParentURL: "whatever", - parentBrokerOptOutJobData: [parentOptOut]) + parentBrokerOptOutJobData: [parentOptOut], + optOutUrl: "broker.com") // Then XCTAssertTrue(profileMatch.hasMatchingRecordOnParentBroker) } + + // MARK: - `profileMatches` Broker OptOut URL & Name tests + + func testProfileMatches_optOutUrlAndBrokerNameForChildBroker() { + // Given + let extractedProfile = ExtractedProfile(id: 1, name: "Sample Name", profileUrl: "profile.com") + + let childBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ChildBroker", + url: "child.com", + parentURL: "parent.com", + optOutUrl: "child.com/optout", + extractedProfile: extractedProfile + ) + + let parentBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ParentBroker", + url: "parent.com", + optOutUrl: "parent.com/optout", + extractedProfile: extractedProfile + ) + + // When + let results = DBPUIDataBrokerProfileMatch.profileMatches(from: [childBroker, parentBroker]) + + // Then + XCTAssertEqual(results.count, 2) + + let childProfile = results.first { $0.dataBroker.name == "ChildBroker" } + XCTAssertEqual(childProfile?.dataBroker.optOutUrl, "child.com/optout") + } + + func testProfileMatches_optOutUrlAndBrokerNameForParentBroker() { + // Given + let extractedProfile = ExtractedProfile(id: 1, name: "Sample Name", profileUrl: "profile.com") + + let childBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ChildBroker", + url: "child.com", + parentURL: "parent.com", + optOutUrl: "parent.com/optout", + extractedProfile: extractedProfile + ) + + let parentBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ParentBroker", + url: "parent.com", + optOutUrl: "parent.com/optout", + extractedProfile: extractedProfile + ) + + // When + let results = DBPUIDataBrokerProfileMatch.profileMatches(from: [childBroker, parentBroker]) + + // Then + XCTAssertEqual(results.count, 2) + + let childProfile = results.first { $0.dataBroker.name == "ChildBroker" } + XCTAssertEqual(childProfile?.dataBroker.optOutUrl, "parent.com/optout") + } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index b41f802d47..f33b340479 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -44,7 +44,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config, optOutUrl: "") let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -92,7 +92,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config, optOutUrl: "") let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -143,7 +143,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config, optOutUrl: "") let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -888,7 +888,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config, optOutUrl: "") let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -913,7 +913,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let currentPreferredRunDate = Date() let expectedPreferredRunDate = Date().addingTimeInterval(config.confirmOptOutScan.hoursToSeconds) - let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config, optOutUrl: "") let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -987,7 +987,8 @@ extension DataBroker { retryError: 0, confirmOptOutScan: 0, maintenanceScan: 0 - ) + ), + optOutUrl: "" ) } @@ -1006,7 +1007,8 @@ extension DataBroker { confirmOptOutScan: 0, maintenanceScan: 0 ), - parent: "some" + parent: "some", + optOutUrl: "" ) } @@ -1020,7 +1022,8 @@ extension DataBroker { retryError: 0, confirmOptOutScan: 0, maintenanceScan: 0 - ) + ), + optOutUrl: "" ) } @@ -1033,7 +1036,8 @@ extension DataBroker { retryError: 0, confirmOptOutScan: 0, maintenanceScan: 0 - ) + ), + optOutUrl: "" ) } @@ -1052,7 +1056,8 @@ extension DataBroker { confirmOptOutScan: 0, maintenanceScan: 0 ), - mirrorSites: mirroSites + mirrorSites: mirroSites, + optOutUrl: "" ) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift index 4f70f8934a..93145f1be4 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift @@ -111,7 +111,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock, optOutUrl: "")] vault.shouldReturnOldVersionBroker = true sut.checkForUpdatesInBrokerJSONFiles() @@ -129,7 +129,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock, optOutUrl: "")] vault.shouldReturnNewVersionBroker = true sut.checkForUpdatesInBrokerJSONFiles() @@ -146,7 +146,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock, optOutUrl: "")] vault.profileQueries = [.mock] sut.checkForUpdatesInBrokerJSONFiles() diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToModelTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToModelTests.swift new file mode 100644 index 0000000000..c3a70fa5de --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToModelTests.swift @@ -0,0 +1,118 @@ +// +// MapperToModelTests.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DataBrokerProtection + +final class MapperToModelTests: XCTestCase { + + private var sut = MapperToModel(mechanism: {_ in Data()}) + private var jsonDecoder: JSONDecoder! + private var jsonEncoder: JSONEncoder! + + override func setUpWithError() throws { + jsonDecoder = JSONDecoder() + jsonEncoder = JSONEncoder() + } + + func testMapToModel_validData() throws { + // Given + let brokerData = DataBroker( + id: 1, + name: "TestBroker", + url: "https://example.com", + steps: [], + version: "1.0", + schedulingConfig: DataBrokerScheduleConfig(retryError: 1, confirmOptOutScan: 2, maintenanceScan: 3), + parent: "ParentBroker", + mirrorSites: [], + optOutUrl: "https://example.com/opt-out" + ) + let jsonData = try jsonEncoder.encode(brokerData) + let brokerDB = BrokerDB(id: 1, name: "TestBroker", json: jsonData, version: "1.0", url: "https://example.com") + + // When + let result = try sut.mapToModel(brokerDB) + + // Then + XCTAssertEqual(result.id, brokerDB.id) + XCTAssertEqual(result.name, brokerDB.name) + XCTAssertEqual(result.url, brokerData.url) + XCTAssertEqual(result.version, brokerData.version) + XCTAssertEqual(result.steps.count, brokerData.steps.count) + XCTAssertEqual(result.parent, brokerData.parent) + XCTAssertEqual(result.mirrorSites.count, brokerData.mirrorSites.count) + XCTAssertEqual(result.optOutUrl, brokerData.optOutUrl) + } + + func testMapToModel_missingOptionalFields() throws { + // Given + let brokerData = """ + { + "name": "TestBroker", + "url": "https://example.com", + "steps": [], + "version": "1.0", + "schedulingConfig": {"retryError": 1, "confirmOptOutScan": 2, "maintenanceScan": 3} + } + """.data(using: .utf8)! + let brokerDB = BrokerDB(id: 1, name: "TestBroker", json: brokerData, version: "1.0", url: "https://example.com") + + // When + let result = try sut.mapToModel(brokerDB) + + // Then + XCTAssertNil(result.parent) + XCTAssertEqual(result.mirrorSites.count, 0) + XCTAssertEqual(result.optOutUrl, "") + } + + func testMapToModel_invalidJSONStructure() throws { + // Given + let invalidJsonData = """ + { + "invalidKey": "value" + } + """.data(using: .utf8)! + let brokerDB = BrokerDB(id: 1, name: "InvalidBroker", json: invalidJsonData, version: "1.0", url: "https://example.com") + + // When & Then + XCTAssertThrowsError(try sut.mapToModel(brokerDB)) { error in + XCTAssertTrue(error is DecodingError) + } + } + + func testMapToModel_missingUrlFallbackToName() throws { + // Given + let brokerData = """ + { + "name": "TestBroker", + "steps": [], + "version": "1.0", + "schedulingConfig": {"retryError": 1, "confirmOptOutScan": 2, "maintenanceScan": 3} + } + """.data(using: .utf8)! + let brokerDB = BrokerDB(id: 1, name: "TestBroker", json: brokerData, version: "1.0", url: "") + + // When + let result = try sut.mapToModel(brokerDB) + + // Then + XCTAssertEqual(result.url, brokerDB.name) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift index a47d13d87a..edff8837e8 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift @@ -394,6 +394,73 @@ final class MapperToUITests: XCTestCase { XCTAssertEqual(result.scanProgress.scannedBrokers, expected) } + // MARK: - `maintenanceScanState` Broker OptOut URL & Name tests + + func testMaintenanceScanState_childBrokerWithOwnOptOutUrl() { + // Given + let extractedProfile = ExtractedProfile(id: 2, name: "Another Sample", profileUrl: "anotherprofile.com", removedDate: nil) + + let childBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ChildBrokerWithOwnOptOut", + url: "child.com", + parentURL: "parent.com", + optOutUrl: "child.com/optout", + extractedProfile: extractedProfile + ) + + let parentBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ParentBroker", + url: "parent.com", + optOutUrl: "parent.com/optout", + extractedProfile: extractedProfile + ) + + // When + let state = sut.maintenanceScanState([childBroker, parentBroker]) + + // Then + XCTAssertEqual(state.inProgressOptOuts.count, 2) + XCTAssertEqual(state.completedOptOuts.count, 0) + + let childProfile = state.inProgressOptOuts.first { $0.dataBroker.name == "ChildBrokerWithOwnOptOut" } + XCTAssertEqual(childProfile?.dataBroker.optOutUrl, "child.com/optout") + + let parentProfile = state.inProgressOptOuts.first { $0.dataBroker.name == "ParentBroker" } + XCTAssertEqual(parentProfile?.dataBroker.optOutUrl, "parent.com/optout") + } + + func testMaintenanceScanState_childBrokerWithParentOptOutUrl() { + // Given + let extractedProfile = ExtractedProfile(id: 1, name: "Sample Name", profileUrl: "profile.com", removedDate: nil) + + let childBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ChildBroker", + url: "child.com", + parentURL: "parent.com", + optOutUrl: "parent.com/optout", + extractedProfile: extractedProfile + ) + + let parentBroker = BrokerProfileQueryData.mock( + dataBrokerName: "ParentBroker", + url: "parent.com", + optOutUrl: "parent.com/optout", + extractedProfile: extractedProfile + ) + + // When + let state = sut.maintenanceScanState([childBroker, parentBroker]) + + // Then + XCTAssertEqual(state.inProgressOptOuts.count, 2) + XCTAssertEqual(state.completedOptOuts.count, 0) + + let childProfile = state.inProgressOptOuts.first { $0.dataBroker.name == "ChildBroker" } + XCTAssertEqual(childProfile?.dataBroker.optOutUrl, "parent.com/optout") + + let parentProfile = state.inProgressOptOuts.first { $0.dataBroker.name == "ParentBroker" } + XCTAssertEqual(childProfile?.dataBroker.optOutUrl, "parent.com/optout") + } } extension DBPUIScanProgress.ScannedBroker { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index fb2af7862b..06ef01eae4 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -155,7 +155,8 @@ extension BrokerProfileQueryData { url: "parent.com", steps: [Step](), version: "1.0.0", - schedulingConfig: DataBrokerScheduleConfig.mock + schedulingConfig: DataBrokerScheduleConfig.mock, + optOutUrl: "" ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50), scanJobData: ScanJobData(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents) @@ -170,7 +171,8 @@ extension BrokerProfileQueryData { steps: [Step](), version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, - parent: "parent.com" + parent: "parent.com", + optOutUrl: "" ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50), scanJobData: ScanJobData(brokerId: 2, profileQueryId: 1, historyEvents: historyEvents) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 3647654770..e9c98d5773 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -33,6 +33,7 @@ extension BrokerProfileQueryData { dataBrokerName: String = "test", url: String = "test.com", parentURL: String? = nil, + optOutUrl: String? = nil, lastRunDate: Date? = nil, preferredRunDate: Date? = nil, extractedProfile: ExtractedProfile? = nil, @@ -48,7 +49,8 @@ extension BrokerProfileQueryData { version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, parent: parentURL, - mirrorSites: mirrorSites + mirrorSites: mirrorSites, + optOutUrl: optOutUrl ?? "" ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50, deprecated: deprecated), scanJobData: ScanJobData(brokerId: 1, @@ -623,9 +625,9 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault func fetchBroker(with name: String) throws -> DataBroker? { if shouldReturnOldVersionBroker { - return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock) + return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock, optOutUrl: "") } else if shouldReturnNewVersionBroker { - return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock) + return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock, optOutUrl: "") } return nil @@ -1231,7 +1233,8 @@ extension DataBroker { retryError: 0, confirmOptOutScan: 0, maintenanceScan: 0 - ) + ), + optOutUrl: "" ) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift index e7fdb4cb45..76d2e4bb65 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift @@ -42,7 +42,8 @@ final class OperationPreferredDateUpdaterTests: XCTestCase { retryError: 1, confirmOptOutScan: confirmOptOutScanHours, maintenanceScan: 1 - ) + ), + optOutUrl: "" ) databaseMock.childBrokers = [childBroker] diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 684c8f86b8..5510c60228 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "201.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "203.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 2f7f0c808d..39a44b7ea7 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "201.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "203.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift index 918aa9a01b..809af385ce 100644 --- a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/PopoverMessageView.swift @@ -86,7 +86,7 @@ public struct PopoverMessageView: View { .frame(minHeight: 22) .lineLimit(nil) .if(viewModel.shouldPresentMultiline) { view in - view.frame(width: 150, alignment: .leading) + view.frame(width: 160, alignment: .leading) } if let text = viewModel.buttonText, diff --git a/UITests/Common/UITests.swift b/UITests/Common/UITests.swift index a92f64ce90..ff590a386e 100644 --- a/UITests/Common/UITests.swift +++ b/UITests/Common/UITests.swift @@ -56,6 +56,7 @@ enum UITests { /// - Parameter requestedToggleState: How the autocomplete checkbox state should be set static func setAutocompleteToggleBeforeTestcaseRuns(_ requestedToggleState: Bool) { let app = XCUIApplication() + app.launchEnvironment["UITEST_MODE"] = "1" app.launch() app.typeKey(",", modifierFlags: [.command]) // Open settings @@ -96,6 +97,7 @@ enum UITests { notificationCenter.typeKey(.escape, modifierFlags: []) } let app = XCUIApplication() + app.launchEnvironment["UITEST_MODE"] = "1" app.launch() app.typeKey("n", modifierFlags: .command) app.typeKey("w", modifierFlags: [.command, .option]) diff --git a/UITests/OnboardingUITests.swift b/UITests/OnboardingUITests.swift index d83b751520..9afa760a0a 100644 --- a/UITests/OnboardingUITests.swift +++ b/UITests/OnboardingUITests.swift @@ -20,6 +20,10 @@ import XCTest final class OnboardingUITests: XCTestCase { + override func tearDownWithError() throws { + try resetApplicationData() + } + func testOnboardingToBrowsing() throws { try resetApplicationData() continueAfterFailure = false @@ -35,83 +39,70 @@ final class OnboardingUITests: XCTestCase { XCTAssertFalse(optionsButton.isEnabled) // Get Started - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Tired of being tracked online? We can help!"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - let getStartedButton = welcomeWindow.webViews["Welcome"].buttons["Get Started"] + XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Ready for a faster browser that keeps you protected?"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + + let getStartedButton = welcomeWindow.webViews["Welcome"].buttons["Let’s Do It!"] XCTAssertTrue(getStartedButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) getStartedButton.click() + // When it clicks on the button the y it's not alligned + let centerCoordinate = getStartedButton.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2)) + centerCoordinate.tap() + + // Protections activated + XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Protections activated!"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - // Default Privacy - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Unlike other browsers, DuckDuckGo comes with privacy by default"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Private Search"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Advanced Tracking Protection"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Automatic Cookie Pop-Up Blocking"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - let gotItButton = welcomeWindow.webViews["Welcome"].buttons["Got It"] - XCTAssertTrue(gotItButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - gotItButton.click() - - // Fewer ads and popups - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Private also means fewer ads and pop-ups"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["While browsing the web"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - let seeWithTrackerBlockingButton = welcomeWindow.webViews["Welcome"].buttons["See With Tracker Blocking"] - XCTAssertTrue(seeWithTrackerBlockingButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - seeWithTrackerBlockingButton.click() - XCTAssertTrue(gotItButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - gotItButton.click() - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["While watching YouTube"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - let seeWithDuckPlayerButton = welcomeWindow.webViews["Welcome"].buttons["See With Duck Player"] - XCTAssertTrue(seeWithDuckPlayerButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - seeWithDuckPlayerButton.click() - let nextGotItButton = welcomeWindow.webViews["Welcome"].buttons["Got It"] - XCTAssertTrue(nextGotItButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - nextGotItButton.click() - welcomeWindow.webViews["Welcome"].click() - let nextButton = welcomeWindow.webViews["Welcome"].buttons["Next"] - XCTAssertTrue(nextButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - nextButton.click() - - // Make Privacy your go-to - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Make privacy your go-to"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) let skipButton = welcomeWindow.webViews["Welcome"].buttons["Skip"] XCTAssertTrue(skipButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) skipButton.click() - welcomeWindow.webViews["Welcome"].click() - let importButton = welcomeWindow.webViews["Welcome"].buttons["Import"] - XCTAssertTrue(importButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - importButton.click() + + // Let’s get you set up + XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Let’s get you set up!"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + + XCTAssertTrue(skipButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) + skipButton.click() + + let importNowButton = welcomeWindow.webViews["Welcome"].buttons["Import Now"] + XCTAssertTrue(importNowButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) + importNowButton.click() + let cancelButton = welcomeWindow.sheets.buttons["Cancel"] XCTAssertTrue(cancelButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) cancelButton.click() + + let nextButtonSetUp = welcomeWindow.webViews["Welcome"].buttons["Next"] + XCTAssertTrue(nextButtonSetUp.waitForExistence(timeout: UITests.Timeouts.elementExistence)) + nextButtonSetUp.click() + + // Duck Player + XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Drowning in ads on YouTube? Not with Duck Player!"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + + let nextButtonDuckPlayer = welcomeWindow.webViews["Welcome"].buttons["Next"] + XCTAssertTrue(nextButtonDuckPlayer.waitForExistence(timeout: UITests.Timeouts.elementExistence)) + nextButtonDuckPlayer.click() + + // Customize Experience + XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Let’s customize a few things…"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + + // Session Restore XCTAssertTrue(skipButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) skipButton.click() - welcomeWindow.webViews["Welcome"].click() - XCTAssertTrue(nextButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - nextButton.click() - - // Customize your experience - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Customize your experience"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - XCTAssertTrue(welcomeWindow.webViews["Welcome"].staticTexts["Make DuckDuckGo work just the way you want."].waitForExistence(timeout: UITests.Timeouts.elementExistence)) - let showBookmarksBarButton = welcomeWindow.webViews["Welcome"].buttons["Show Bookmarks Bar"] - XCTAssertTrue(showBookmarksBarButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - showBookmarksBarButton.click() - XCTAssertTrue(welcomeWindow.collectionViews["BookmarksBarViewController.bookmarksBarCollectionView"].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + let enableSessionRestoreButton = welcomeWindow.webViews["Welcome"].buttons["Enable Session Restore"] XCTAssertTrue(enableSessionRestoreButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) enableSessionRestoreButton.click() - welcomeWindow.webViews["Welcome"].click() + let showHomeButton = welcomeWindow.webViews["Welcome"].buttons["Show Home Button"] XCTAssertTrue(showHomeButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) showHomeButton.click() - XCTAssertTrue(welcomeWindow.children(matching: .button).element(boundBy: 3).waitForExistence(timeout: UITests.Timeouts.elementExistence)) - welcomeWindow.webViews["Welcome"].click() - XCTAssertTrue(nextButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) - nextButton.click() + + // Start Browsing let startBrowsingButton = welcomeWindow.webViews["Welcome"].buttons["Start Browsing"] XCTAssertTrue(startBrowsingButton.waitForExistence(timeout: UITests.Timeouts.elementExistence)) startBrowsingButton.click() // AfterOnboarding - let duckduckgoPrivacySimplifiedWindow = app.windows["DuckDuckGo — Privacy, simplified."] - XCTAssertTrue(duckduckgoPrivacySimplifiedWindow.webViews["DuckDuckGo — Privacy, simplified."].waitForExistence(timeout: UITests.Timeouts.elementExistence)) + let duckduckgoPrivacySimplifiedWindow = app.windows["DuckDuckGo — Your protection, our priority."] + XCTAssertTrue(duckduckgoPrivacySimplifiedWindow.webViews["DuckDuckGo — Your protection, our priority."].waitForExistence(timeout: UITests.Timeouts.elementExistence)) XCTAssertTrue(duckduckgoPrivacySimplifiedWindow.buttons["NavigationBarViewController.optionsButton"].isEnabled) } diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_brave.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_brave.html index 7ba22d7cdd..d6c224a11b 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_brave.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_brave.html @@ -47,7 +47,7 @@

Bookmarks

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

DupeFolderNameContents

MacRumors: Apple News and Rumors diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_chrome.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_chrome.html index 08fca4e9b7..54f9bdb6ae 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_chrome.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_chrome.html @@ -48,7 +48,7 @@

Bookmarks

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

DupeFolderNameContents

MacRumors: Apple News and Rumors diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_android.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_android.html index 8a87fa9f41..1e7cb6bf92 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_android.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_android.html @@ -47,7 +47,7 @@

Bookmarks

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

DupeFolderNameContents

MacRumors: Apple News and Rumors diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_ios.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_ios.html index 9d592f0a73..5897360127 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_ios.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_ios.html @@ -13,7 +13,7 @@

Bookmarks

FolderA-2

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

FolderB

@@ -22,4 +22,4 @@

Bookmarks

Wikipedia

- \ No newline at end of file + diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_macos.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_macos.html index 9965f7d234..d3027ccc37 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_macos.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_ddg_macos.html @@ -42,7 +42,7 @@

Bookmarks

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

DupeFolderNameContents

MacRumors: Apple News and Rumors diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_firefox.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_firefox.html index 92c60fcd13..24f2804799 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_firefox.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_firefox.html @@ -55,7 +55,7 @@

Bookmarks Menu

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

DupeFolderNameContents

MacRumors: Apple News and Rumors diff --git a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_safari.html b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_safari.html index 560cbf6533..cf1e87b8be 100644 --- a/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_safari.html +++ b/UnitTests/DataImport/DataImportResources/TestBookmarksData/bookmarks_safari.html @@ -53,7 +53,7 @@

Bookmarks

The Wall Street Journal - Breaking News, Business, Financial & Economic News, World News and Video

-

DuckDuckGo — Privacy, simplified. +
DuckDuckGo — Your protection, our priority.

DupeFolderNameContents

MacRumors: Apple News and Rumors diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_brave.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_brave.json index 80fbf50a09..15976f795f 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_brave.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_brave.json @@ -109,7 +109,7 @@ "type" : "folder" }, { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com\/" }, diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_chrome.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_chrome.json index cbcd35b844..baed9b3f74 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_chrome.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_chrome.json @@ -116,7 +116,7 @@ "type" : "folder" }, { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com\/" }, diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_android.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_android.json index cfd7ed9645..fea09751a1 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_android.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_android.json @@ -109,7 +109,7 @@ "type" : "folder" }, { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com\/" }, diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_ios.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_ios.json index 51dd407681..941910f466 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_ios.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_ios.json @@ -39,7 +39,7 @@ { "children" : [ { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com" } diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_macos.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_macos.json index 390cac5521..dc6afebdc5 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_macos.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_ddg_macos.json @@ -116,7 +116,7 @@ "type" : "folder" }, { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com\/" }, diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_firefox.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_firefox.json index d3428b67b2..bca8960b81 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_firefox.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_firefox.json @@ -135,7 +135,7 @@ "type" : "folder" }, { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com\/" }, diff --git a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_safari.json b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_safari.json index 9431af6f7b..f84e20f19e 100644 --- a/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_safari.json +++ b/UnitTests/DataImport/__Snapshots__/BookmarksHTMLReaderTests/snapshot.bookmarks_safari.json @@ -138,7 +138,7 @@ "type" : "folder" }, { - "name" : "DuckDuckGo — Privacy, simplified.", + "name" : "DuckDuckGo — Your protection, our priority.", "type" : "bookmark", "url" : "https:\/\/duckduckgo.com\/" }, diff --git a/UnitTests/History/Model/HistoryCoordinatingMock.swift b/UnitTests/History/Model/HistoryCoordinatingMock.swift index 5e74455cb6..d16af67900 100644 --- a/UnitTests/History/Model/HistoryCoordinatingMock.swift +++ b/UnitTests/History/Model/HistoryCoordinatingMock.swift @@ -95,4 +95,10 @@ final class HistoryCoordinatingMock: HistoryCoordinating { trackerFoundCalled = true } + var removeUrlEntryCalled = false + func removeUrlEntry(_ url: URL, completion: (((any Error)?) -> Void)?) { + removeUrlEntryCalled = true + completion?(nil) + } + } diff --git a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift index fbd8316ade..c2f35053c2 100644 --- a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift +++ b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift @@ -62,4 +62,34 @@ final class SuggestionContainerTests: XCTestCase { XCTAssertNil(suggestionContainer.result) } + func testSuggestionLoadingCacheClearing() { + let suggestionLoadingMock = SuggestionLoadingMock() + let historyCoordinatingMock = HistoryCoordinatingMock() + let suggestionContainer = SuggestionContainer(suggestionLoading: suggestionLoadingMock, + historyCoordinating: historyCoordinatingMock, + bookmarkManager: LocalBookmarkManager.shared) + + XCTAssertNil(suggestionContainer.suggestionDataCache) + let e = expectation(description: "Suggestions updated") + suggestionContainer.suggestionLoading(suggestionLoadingMock, suggestionDataFromUrl: URL.testsServer, withParameters: [:]) { data, error in + XCTAssertNotNil(suggestionContainer.suggestionDataCache) + e.fulfill() + + // Test the cache is not cleared if useCachedData is true + XCTAssertFalse(suggestionLoadingMock.getSuggestionsCalled) + suggestionContainer.getSuggestions(for: "test", useCachedData: true) + XCTAssertNotNil(suggestionContainer.suggestionDataCache) + XCTAssert(suggestionLoadingMock.getSuggestionsCalled) + + suggestionLoadingMock.getSuggestionsCalled = false + + // Test the cache is cleared if useCachedData is false + XCTAssertFalse(suggestionLoadingMock.getSuggestionsCalled) + suggestionContainer.getSuggestions(for: "test", useCachedData: false) + XCTAssertNil(suggestionContainer.suggestionDataCache) + XCTAssert(suggestionLoadingMock.getSuggestionsCalled) + } + + waitForExpectations(timeout: 1) + } } diff --git a/UnitTests/Updates/BinaryOwnershipCheckerTests.swift b/UnitTests/Updates/BinaryOwnershipCheckerTests.swift deleted file mode 100644 index 76a8029c2f..0000000000 --- a/UnitTests/Updates/BinaryOwnershipCheckerTests.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// BinaryOwnershipCheckerTests.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 XCTest - -@testable import DuckDuckGo_Privacy_Browser - -class BinaryOwnershipCheckerTests: XCTestCase { - - func testWhenUserIsOwner_ThenIsCurrentUserOwnerReturnsTrue() { - let mockFileManager = MockFileManager() - mockFileManager.attributes = [ - .ownerAccountID: NSNumber(value: getuid()) - ] - let checker = BinaryOwnershipChecker(fileManager: mockFileManager) - let isOwner = checker.isCurrentUserOwner() - - XCTAssertTrue(isOwner, "Expected the current user to be identified as the owner.") - } - - func testWhenUserIsNotOwner_ThenIsCurrentUserOwnerReturnsFalse() { - let mockFileManager = MockFileManager() - mockFileManager.attributes = [ - .ownerAccountID: NSNumber(value: getuid() + 1) // Simulate a different user - ] - let checker = BinaryOwnershipChecker(fileManager: mockFileManager) - let isOwner = checker.isCurrentUserOwner() - - XCTAssertFalse(isOwner, "Expected the current user not to be identified as the owner.") - } - - func testWhenFileManagerThrowsError_ThenIsCurrentUserOwnerReturnsFalse() { - let mockFileManager = MockFileManager() - mockFileManager.shouldThrowError = true - let checker = BinaryOwnershipChecker(fileManager: mockFileManager) - let isOwner = checker.isCurrentUserOwner() - - XCTAssertFalse(isOwner, "Expected the ownership check to fail and return false when an error occurs.") - } - - func testWhenOwnershipIsCheckedMultipleTimes_ThenResultIsCached() { - let mockFileManager = MockFileManager() - mockFileManager.attributes = [ - .ownerAccountID: NSNumber(value: getuid()) - ] - let checker = BinaryOwnershipChecker(fileManager: mockFileManager) - let isOwnerFirstCheck = checker.isCurrentUserOwner() - - mockFileManager.attributes = [ - .ownerAccountID: NSNumber(value: getuid() + 1) - ] - let isOwnerSecondCheck = checker.isCurrentUserOwner() - - XCTAssertTrue(isOwnerFirstCheck, "Expected the current user to be identified as the owner on first check.") - XCTAssertTrue(isOwnerSecondCheck, "Expected the cached result to be used, so the second check should return the same result as the first.") - } -} - -// Mock FileManager to simulate different file attributes and errors -class MockFileManager: FileManager { - - var attributes: [FileAttributeKey: Any]? - var shouldThrowError = false - - override func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any] { - if shouldThrowError { - throw NSError(domain: NSCocoaErrorDomain, code: NSFileReadNoSuchFileError, userInfo: nil) - } - return attributes ?? [:] - } -} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 044c6099cf..61158943f5 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -161,7 +161,7 @@ platform :mac do deliver(common_deliver_arguments(options).merge({ skip_binary_upload: true, skip_metadata: false, - version_check_wait_retry_limit: 0 + version_check_wait_retry_limit: 1 })) end