From e21b32839382020dc384562233b8b43200f57720 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 30 Aug 2024 17:49:25 +0100 Subject: [PATCH 01/16] Modal for Duck Player experiment (#3163) Task/Issue URL: https://app.asana.com/0/1204167627774280/1208057428394427/f **Description**: Implement the onboarding screen for the Duck Player experiment on macOS --- DuckDuckGo.xcodeproj/project.pbxproj | 90 ++++++ .../Contents.json | 0 .../Contents.json | 0 .../Contents.json | 0 .../Contents.json | 0 .../Images/DuckPlayer/Contents.json | 6 + .../Contents.json | 12 + .../DuckPlayerConsentModal.pdf | Bin 0 -> 4094 bytes .../Contents.json | 12 + .../DuckPlayerConsentModalDax.pdf | Bin 0 -> 16938 bytes DuckDuckGo/Common/Localizables/UserText.swift | 9 + DuckDuckGo/Menus/MainMenu.swift | 3 + DuckDuckGo/Menus/MainMenuActions.swift | 8 + .../Model/DuckPlayerPreferences.swift | 8 + DuckDuckGo/Tab/Model/Tab+Navigation.swift | 3 + .../DuckPlayerOnboardingTabExtension.swift | 76 +++++ .../DuckPlayerTabExtension.swift | 25 +- .../Tab/TabExtensions/TabExtensions.swift | 8 +- .../Tab/View/BrowserTabViewController.swift | 18 ++ DuckDuckGo/YoutubePlayer/DuckPlayer.swift | 36 ++- ...uckPlayerOnboardingLocationValidator.swift | 52 ++++ .../DuckPlayerOnboardingDecider.swift | 143 +++++++++ .../DuckPlayerOnboardingModalManager.swift | 29 ++ .../DuckPlayerOnboardingModalView.swift | 293 ++++++++++++++++++ .../DuckPlayerOnboardingViewController.swift | 72 +++++ .../DuckPlayerOnboardingViewModel.swift | 57 ++++ .../YoutubePlayer/TabModal/TabModal.swift | 140 +++++++++ .../TabModal/TabModalManageable.swift | 82 +++++ .../YoutubeOverlayUserScript.swift | 10 + ...aultDuckPlayerOnboardingDeciderTests.swift | 146 +++++++++ ...ayerOnboardingLocationValidatorTests.swift | 93 ++++++ 31 files changed, 1420 insertions(+), 11 deletions(-) rename DuckDuckGo/Assets.xcassets/Colors/{CookieConsentPrimaryButton.colorset => DuckPlayerOnboardingPrimaryButton.colorset}/Contents.json (100%) rename DuckDuckGo/Assets.xcassets/Colors/{CookieConsentPrimaryButtonPressed.colorset => DuckPlayerOnboardingPrimaryButtonPressed.colorset}/Contents.json (100%) rename DuckDuckGo/Assets.xcassets/Colors/{CookieConsentSecondaryButton.colorset => DuckPlayerOnboardingSecondaryButton.colorset}/Contents.json (100%) rename DuckDuckGo/Assets.xcassets/Colors/{CookieConsentSecondaryButtonPressed.colorset => DuckPlayerOnboardingSecondaryButtonPressed.colorset}/Contents.json (100%) create mode 100644 DuckDuckGo/Assets.xcassets/Images/DuckPlayer/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/DuckPlayerConsentModal.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/DuckPlayerConsentModalDax.pdf create mode 100644 DuckDuckGo/Tab/TabExtensions/DuckPlayerOnboardingTabExtension.swift create mode 100644 DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingLocationValidator.swift create mode 100644 DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingDecider.swift create mode 100644 DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalManager.swift create mode 100644 DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalView.swift create mode 100644 DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewController.swift create mode 100644 DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewModel.swift create mode 100644 DuckDuckGo/YoutubePlayer/TabModal/TabModal.swift create mode 100644 DuckDuckGo/YoutubePlayer/TabModal/TabModalManageable.swift create mode 100644 UnitTests/YoutubePlayer/DefaultDuckPlayerOnboardingDeciderTests.swift create mode 100644 UnitTests/YoutubePlayer/DuckPlayerOnboardingLocationValidatorTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index afbe6a0a02..7742eb8eaa 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -220,6 +220,28 @@ 317295D52AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317295D12AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift */; }; 3184AC6D288F29D800C35E4B /* BadgeNotificationAnimationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3184AC6C288F29D800C35E4B /* BadgeNotificationAnimationModel.swift */; }; 3184AC6F288F2A1100C35E4B /* CookieNotificationAnimationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3184AC6E288F2A1100C35E4B /* CookieNotificationAnimationModel.swift */; }; + 3199AF6F2C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF632C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift */; }; + 3199AF702C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF632C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift */; }; + 3199AF732C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF652C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift */; }; + 3199AF742C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF652C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift */; }; + 3199AF752C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF662C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift */; }; + 3199AF762C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF662C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift */; }; + 3199AF772C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF672C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift */; }; + 3199AF782C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF672C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift */; }; + 3199AF792C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF682C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift */; }; + 3199AF7A2C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF682C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift */; }; + 3199AF7B2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF6A2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift */; }; + 3199AF7C2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF6A2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift */; }; + 3199AF7D2C80734A003AEBDC /* TabModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF6C2C80734A003AEBDC /* TabModal.swift */; }; + 3199AF7E2C80734A003AEBDC /* TabModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF6C2C80734A003AEBDC /* TabModal.swift */; }; + 3199AF7F2C80734A003AEBDC /* TabModalManageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF6D2C80734A003AEBDC /* TabModalManageable.swift */; }; + 3199AF802C80734A003AEBDC /* TabModalManageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF6D2C80734A003AEBDC /* TabModalManageable.swift */; }; + 3199AF832C80736C003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF812C80736B003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift */; }; + 3199AF842C80736C003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF812C80736B003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift */; }; + 3199AF852C80736C003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF822C80736B003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift */; }; + 3199AF862C80736C003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF822C80736B003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift */; }; + 3199AF882C8073CB003AEBDC /* DuckPlayerOnboardingTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF872C8073CA003AEBDC /* DuckPlayerOnboardingTabExtension.swift */; }; + 3199AF892C8073CB003AEBDC /* DuckPlayerOnboardingTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3199AF872C8073CA003AEBDC /* DuckPlayerOnboardingTabExtension.swift */; }; 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */; }; 31A2FD182BAB43BA00D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */; }; 31A3A4E32B0C115F0021063C /* DataBrokerProtection in Frameworks */ = {isa = PBXBuildFile; productRef = 31A3A4E22B0C115F0021063C /* DataBrokerProtection */; }; @@ -3130,6 +3152,17 @@ 3184AC6E288F2A1100C35E4B /* CookieNotificationAnimationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieNotificationAnimationModel.swift; sourceTree = ""; }; 3192A2702A4C4E330084EA89 /* DataBrokerProtection */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DataBrokerProtection; sourceTree = ""; }; 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPHomeViewController.swift; sourceTree = ""; }; + 3199AF632C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingDecider.swift; sourceTree = ""; }; + 3199AF652C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingModalManager.swift; sourceTree = ""; }; + 3199AF662C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingModalView.swift; sourceTree = ""; }; + 3199AF672C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingViewController.swift; sourceTree = ""; }; + 3199AF682C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingViewModel.swift; sourceTree = ""; }; + 3199AF6A2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingLocationValidator.swift; sourceTree = ""; }; + 3199AF6C2C80734A003AEBDC /* TabModal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabModal.swift; sourceTree = ""; }; + 3199AF6D2C80734A003AEBDC /* TabModalManageable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabModalManageable.swift; sourceTree = ""; }; + 3199AF812C80736B003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingLocationValidatorTests.swift; sourceTree = ""; }; + 3199AF822C80736B003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultDuckPlayerOnboardingDeciderTests.swift; sourceTree = ""; }; + 3199AF872C8073CA003AEBDC /* DuckPlayerOnboardingTabExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerOnboardingTabExtension.swift; sourceTree = ""; }; 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureDisabler.swift; sourceTree = ""; }; 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionAppEvents.swift; sourceTree = ""; }; 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureGatekeeperTests.swift; sourceTree = ""; }; @@ -4945,6 +4978,36 @@ path = DBP; sourceTree = ""; }; + 3199AF692C80734A003AEBDC /* DuckPlayerOnboardingModal */ = { + isa = PBXGroup; + children = ( + 3199AF632C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift */, + 3199AF652C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift */, + 3199AF662C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift */, + 3199AF672C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift */, + 3199AF682C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift */, + ); + path = DuckPlayerOnboardingModal; + sourceTree = ""; + }; + 3199AF6B2C80734A003AEBDC /* Onboarding */ = { + isa = PBXGroup; + children = ( + 3199AF692C80734A003AEBDC /* DuckPlayerOnboardingModal */, + 3199AF6A2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; + 3199AF6E2C80734A003AEBDC /* TabModal */ = { + isa = PBXGroup; + children = ( + 3199AF6C2C80734A003AEBDC /* TabModal.swift */, + 3199AF6D2C80734A003AEBDC /* TabModalManageable.swift */, + ); + path = TabModal; + sourceTree = ""; + }; 31A2FD152BAB419400D0E741 /* DBP */ = { isa = PBXGroup; children = ( @@ -4976,6 +5039,8 @@ 31F28C4B28C8EE9000119F70 /* YoutubePlayer */ = { isa = PBXGroup; children = ( + 3199AF6B2C80734A003AEBDC /* Onboarding */, + 3199AF6E2C80734A003AEBDC /* TabModal */, 37F19A6928E2F2D000740DC6 /* DuckPlayer.swift */, 31F28C5228C8EECA00119F70 /* DuckURLSchemeHandler.swift */, 315AA06F28CA5CC800200030 /* YoutubePlayerNavigationHandler.swift */, @@ -5043,6 +5108,8 @@ 376718FE28E58504003A2A15 /* YoutubePlayer */ = { isa = PBXGroup; children = ( + 3199AF822C80736B003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift */, + 3199AF812C80736B003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift */, 3714B1E828EDBAAB0056C57A /* DuckPlayerTests.swift */, 567DA94429E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift */, ); @@ -8169,6 +8236,7 @@ B6D574B12947224C008ED1B6 /* ContentBlockingTabExtension.swift */, B6DA06E32913ECEE00225DE2 /* ContextMenuManager.swift */, B6685E4129A61C460043D2EE /* DownloadsTabExtension.swift */, + 3199AF872C8073CA003AEBDC /* DuckPlayerOnboardingTabExtension.swift */, B6C416A6294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift */, B6D574B329472253008ED1B6 /* FBProtectionTabExtension.swift */, B6C00ECC292F89D9009C73A6 /* FindInPageTabExtension.swift */, @@ -10039,6 +10107,7 @@ 7BCB90C32C1863BA008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift in Sources */, EEC4A66E2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 3706FA83293F65D500E42796 /* LazyLoadable.swift in Sources */, + 3199AF7C2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift in Sources */, 3706FA85293F65D500E42796 /* KeyedCodingExtension.swift in Sources */, 3706FA87293F65D500E42796 /* DownloadListStore.swift in Sources */, 37197EAB2942443D00394917 /* WebViewContainerView.swift in Sources */, @@ -10247,6 +10316,7 @@ 3707C71F294B5D2900682A9F /* WKUserContentControllerExtension.swift in Sources */, 3706FB1A293F65D500E42796 /* OutlineSeparatorViewCell.swift in Sources */, 3706FB1B293F65D500E42796 /* SafariDataImporter.swift in Sources */, + 3199AF702C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift in Sources */, 3706FB1D293F65D500E42796 /* StatisticsLoader.swift in Sources */, 56406D4C2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift in Sources */, 3793FDD829535EBA00A2E28F /* Assertions.swift in Sources */, @@ -10279,6 +10349,7 @@ 3706FB31293F65D500E42796 /* PinnedTabsHostingView.swift in Sources */, B6AFE6BC29A5D3F8002FF962 /* PrivacyDashboardTabExtension.swift in Sources */, 3706FB32293F65D500E42796 /* FirefoxBookmarksReader.swift in Sources */, + 3199AF7E2C80734A003AEBDC /* TabModal.swift in Sources */, 1D39E57B2C2C0F3700757339 /* ReleaseNotesUserScript.swift in Sources */, 3706FB33293F65D500E42796 /* DeviceIdleStateDetector.swift in Sources */, 1DB67F2A2B6FEB17003DF243 /* WebViewSnapshotRenderer.swift in Sources */, @@ -10296,6 +10367,7 @@ 3706FB3E293F65D500E42796 /* BookmarksBarViewModel.swift in Sources */, 3706FB3F293F65D500E42796 /* NSPopUpButtonView.swift in Sources */, F1FDC9392BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, + 3199AF762C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift in Sources */, 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */, 3706FB40293F65D500E42796 /* BookmarksContextMenu.swift in Sources */, 3706FB41293F65D500E42796 /* NavigationBarViewController.swift in Sources */, @@ -10328,6 +10400,7 @@ B6B5F5802B024105008DB58A /* DataImportSummaryView.swift in Sources */, 4B9DB0422A983B24000927DB /* WaitlistDialogView.swift in Sources */, 3706FB57293F65D500E42796 /* AppPrivacyConfigurationDataProvider.swift in Sources */, + 3199AF7A2C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift in Sources */, C1B1CBE22BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */, 857E5AF62A790B7000FC0FB4 /* PixelExperiment.swift in Sources */, 9F33445F2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift in Sources */, @@ -10568,11 +10641,13 @@ 31267C6B2B640C5200FEF811 /* DataBrokerProtectionAppEvents.swift in Sources */, 3706FBE4293F65D500E42796 /* FireAnimationView.swift in Sources */, 3706FBE5293F65D500E42796 /* FaviconUrlReference.swift in Sources */, + 3199AF802C80734A003AEBDC /* TabModalManageable.swift in Sources */, 3706FBE7293F65D500E42796 /* PasswordManagementItemListModel.swift in Sources */, 3706FBE8293F65D500E42796 /* SuggestionTableCellView.swift in Sources */, 3706FBE9293F65D500E42796 /* FireViewModel.swift in Sources */, B68D21D02ACBC9FD002DA3C2 /* ContentBlockerRulesManagerMock.swift in Sources */, BD7090D02C5182FB009EED82 /* UnifiedFeedbackFormView.swift in Sources */, + 3199AF742C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift in Sources */, 3706FEC6293F6F0600E42796 /* BWKeyStorage.swift in Sources */, 4B6785482AA8DE69008A5004 /* VPNUninstaller.swift in Sources */, 3706FBEC293F65D500E42796 /* EditableTextView.swift in Sources */, @@ -10767,6 +10842,7 @@ 3706FC61293F65D500E42796 /* NSAlertExtension.swift in Sources */, 3706FC62293F65D500E42796 /* ThirdPartyBrowser.swift in Sources */, 3706FC63293F65D500E42796 /* CircularProgressView.swift in Sources */, + 3199AF782C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift in Sources */, 3706FC64293F65D500E42796 /* SuggestionContainer.swift in Sources */, C16127EF2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */, 3706FC65293F65D500E42796 /* HomePageViewController.swift in Sources */, @@ -10812,6 +10888,7 @@ 3706FC82293F65D500E42796 /* PermissionAuthorizationPopover.swift in Sources */, 4BCBE4552BA7E16600FC75A1 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */, 371209312C233D69003ADF3D /* RemoteMessagingStoreErrorHandling.swift in Sources */, + 3199AF892C8073CB003AEBDC /* DuckPlayerOnboardingTabExtension.swift in Sources */, 3706FC83293F65D500E42796 /* PopoverMessageViewController.swift in Sources */, 7B5A23702C46A116007213AC /* ExcludedDomainsModel.swift in Sources */, 9D9AE86E2AA76D1F0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, @@ -11070,6 +11147,7 @@ 1DE03425298BC7F000CAB3D7 /* InternalUserDeciderStoreMock.swift in Sources */, 3706FE4A293F661700E42796 /* BookmarkManagedObjectTests.swift in Sources */, BBFF355E2C4AF26200DA3289 /* BookmarksSortModeTests.swift in Sources */, + 3199AF842C80736C003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift in Sources */, EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */, BDA764922BC4E57200D0400C /* MockVPNLocationFormatter.swift in Sources */, 3706FE4B293F661700E42796 /* BookmarksHTMLImporterTests.swift in Sources */, @@ -11084,6 +11162,7 @@ 3706FE51293F661700E42796 /* SafariBookmarksReaderTests.swift in Sources */, 3706FE52293F661700E42796 /* FileSystemDSLTests.swift in Sources */, 3706FE53293F661700E42796 /* CoreDataEncryptionTests.swift in Sources */, + 3199AF862C80736C003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift in Sources */, 3706FE54293F661700E42796 /* PasteboardBookmarkTests.swift in Sources */, 3706FE55293F661700E42796 /* CBRCompileTimeReporterTests.swift in Sources */, B6E6BA172BA2CF60008AA7E1 /* SandboxTestToolNotifications.swift in Sources */, @@ -11638,6 +11717,7 @@ F18826912BC0105800D9AC4F /* PixelDataStore.swift in Sources */, B69B503E2726A12500758A2B /* AtbParser.swift in Sources */, 37F19A6528E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift in Sources */, + 3199AF7D2C80734A003AEBDC /* TabModal.swift in Sources */, 84F1C8DE2C774D4200716446 /* NSTableViewExtension.swift in Sources */, 7B7F5D212C526CE600826256 /* AddExcludedDomainView.swift in Sources */, 4B92929E26670D2A00AD2C21 /* BookmarkSidebarTreeController.swift in Sources */, @@ -11655,6 +11735,7 @@ AAC6881928626BF800D54247 /* RecentlyClosedTab.swift in Sources */, B688B4DF27420D290087BEAF /* PDFSearchTextMenuItemHandler.swift in Sources */, AA7E919728746BCC00AB6B62 /* HistoryMenu.swift in Sources */, + 3199AF752C80734A003AEBDC /* DuckPlayerOnboardingModalView.swift in Sources */, BB470EBB2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift in Sources */, F4A6198C283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift in Sources */, 85707F24276A332A00DC0649 /* OnboardingButtonStyles.swift in Sources */, @@ -11807,9 +11888,11 @@ B60293E62BA19ECD0033186B /* NetPPopoverManagerMock.swift in Sources */, B6B3E0E12657EA7A0040E0A2 /* NSScreenExtension.swift in Sources */, B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, + 3199AF882C8073CB003AEBDC /* DuckPlayerOnboardingTabExtension.swift in Sources */, 31ECDA112BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, + 3199AF732C80734A003AEBDC /* DuckPlayerOnboardingModalManager.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 4B67854A2AA8DE75008A5004 /* VPNFeatureGatekeeper.swift in Sources */, 37A089FB2C510FE0003BB417 /* RemoteMessagingDebugMenu.swift in Sources */, @@ -11835,6 +11918,7 @@ 4B723E0F26B0006500E14D75 /* CSVParser.swift in Sources */, B63BDF7E27FDAA640072D75B /* PrivacyDashboardWebView.swift in Sources */, 37CD54CF27F2FDD100F1F7B9 /* AppearancePreferences.swift in Sources */, + 3199AF7B2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift in Sources */, B6B1E87B26D381710062C350 /* DownloadListCoordinator.swift in Sources */, B68D21C82ACBC96D002DA3C2 /* MockPrivacyConfiguration.swift in Sources */, B647EFBB2922584B00BA628D /* AdClickAttributionTabExtension.swift in Sources */, @@ -11860,6 +11944,7 @@ AA5C1DD5285C780C0089850C /* RecentlyClosedCoordinator.swift in Sources */, AA88D14B252A557100980B4E /* URLRequestExtension.swift in Sources */, AA6197C6276B3168008396F0 /* FaviconHostReference.swift in Sources */, + 3199AF792C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift in Sources */, B6685E4229A61C470043D2EE /* DownloadsTabExtension.swift in Sources */, 4B8AC93B26B48ADF00879451 /* ASN1Parser.swift in Sources */, 856C98DF257014BD00A22F1F /* FileDownloadManager.swift in Sources */, @@ -11951,6 +12036,7 @@ 56A0541F2C1CA1F5007D8FAB /* OnboardingTabExtension.swift in Sources */, B63D466925BEB6C200874977 /* WKWebView+SessionState.swift in Sources */, B6F1B0222BCE5658005E863C /* BrokenSiteInfoTabExtension.swift in Sources */, + 3199AF7F2C80734A003AEBDC /* TabModalManageable.swift in Sources */, 4B4D60C02A0C848D00BCD287 /* NetworkProtectionControllerErrorStore.swift in Sources */, 4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */, 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */, @@ -12092,6 +12178,7 @@ 7B60AFFF2C51426A008E32A3 /* VPNURLEventHandler.swift in Sources */, D64A5FF82AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */, 37A6A8F62AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */, + 3199AF6F2C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift in Sources */, AA3F895324C18AD500628DDE /* SuggestionViewModel.swift in Sources */, 4B9292A326670D2A00AD2C21 /* BookmarkManagedObject.swift in Sources */, 4B723E1326B0007A00E14D75 /* CSVLoginExporter.swift in Sources */, @@ -12106,6 +12193,7 @@ 31F28C5128C8EEC500119F70 /* YoutubeOverlayUserScript.swift in Sources */, B6ABD0CA2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */, B6040856274B830F00680351 /* DictionaryExtension.swift in Sources */, + 3199AF772C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift in Sources */, B684592725C93C0500DC17B6 /* Publishers.NestedObjectChanges.swift in Sources */, B6DA06E62913F39400225DE2 /* MenuItemSelectors.swift in Sources */, 3712092C2C23383C003ADF3D /* RemoteMessagingStoreErrorHandling.swift in Sources */, @@ -12402,6 +12490,7 @@ AAC9C01C24CB594C00AD1325 /* TabViewModelTests.swift in Sources */, 84DC715B2C1C1E9000033B8C /* UserDefaultsWrapperTests.swift in Sources */, 561D29CA2BDA752F007B91D0 /* MockAppearancePreferencesPersistor.swift in Sources */, + 3199AF832C80736C003AEBDC /* DuckPlayerOnboardingLocationValidatorTests.swift in Sources */, 56A0540D2C1C375D007D8FAB /* MockWindow.swift in Sources */, 567DA93F29E8045D008AC5EE /* MockEmailStorage.swift in Sources */, 37CD54B727F1B28A00F1F7B9 /* DefaultBrowserPreferencesTests.swift in Sources */, @@ -12637,6 +12726,7 @@ 376C4DB928A1A48A00CC0F5B /* FirePopoverViewModelTests.swift in Sources */, AAEC74B62642CC6A00C2EFBC /* HistoryStoringMock.swift in Sources */, AA652CB125DD825B009059CC /* LocalBookmarkStoreTests.swift in Sources */, + 3199AF852C80736C003AEBDC /* DefaultDuckPlayerOnboardingDeciderTests.swift in Sources */, B630794226731F5400DCEE41 /* WKDownloadMock.swift in Sources */, B6C0B24626E9CB190031CB7F /* RunLoopExtensionTests.swift in Sources */, 56D145F129E6F06D00E3488A /* MockBookmarkManager.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Colors/CookieConsentPrimaryButton.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingPrimaryButton.colorset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Colors/CookieConsentPrimaryButton.colorset/Contents.json rename to DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingPrimaryButton.colorset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Colors/CookieConsentPrimaryButtonPressed.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingPrimaryButtonPressed.colorset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Colors/CookieConsentPrimaryButtonPressed.colorset/Contents.json rename to DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingPrimaryButtonPressed.colorset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Colors/CookieConsentSecondaryButton.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingSecondaryButton.colorset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Colors/CookieConsentSecondaryButton.colorset/Contents.json rename to DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingSecondaryButton.colorset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Colors/CookieConsentSecondaryButtonPressed.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingSecondaryButtonPressed.colorset/Contents.json similarity index 100% rename from DuckDuckGo/Assets.xcassets/Colors/CookieConsentSecondaryButtonPressed.colorset/Contents.json rename to DuckDuckGo/Assets.xcassets/Colors/DuckPlayerOnboardingSecondaryButtonPressed.colorset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/Contents.json new file mode 100644 index 0000000000..35fadfe35d --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "DuckPlayerConsentModal.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/DuckPlayerConsentModal.pdf b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModal.imageset/DuckPlayerConsentModal.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6c039fc856dd347f25eecd1e9dc4ff41992a023d GIT binary patch literal 4094 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 literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/Contents.json new file mode 100644 index 0000000000..b2115618cd --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "DuckPlayerConsentModalDax.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/DuckPlayerConsentModalDax.pdf b/DuckDuckGo/Assets.xcassets/Images/DuckPlayer/DuckPlayerOnboardingModalDax.imageset/DuckPlayerConsentModalDax.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f35514d214ceab4f54684f7ee30b385084957df2 GIT binary patch literal 16938 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.Key.homePageShowPermanentSurvey.rawValue) } + @objc func resetDuckPlayerOnboarding(_ sender: Any?) { + DefaultDuckPlayerOnboardingDecider().reset() + } + + @objc func resetDuckPlayerPreferences(_ sender: Any?) { + DuckPlayerPreferences.shared.reset() + } + @objc func internalUserState(_ sender: Any?) { guard let internalUserDecider = NSApp.delegateTyped.internalUserDecider as? DefaultInternalUserDecider else { return } let state = internalUserDecider.isInternalUser diff --git a/DuckDuckGo/Preferences/Model/DuckPlayerPreferences.swift b/DuckDuckGo/Preferences/Model/DuckPlayerPreferences.swift index 653e7cc4c2..ca523a9875 100644 --- a/DuckDuckGo/Preferences/Model/DuckPlayerPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DuckPlayerPreferences.swift @@ -117,6 +117,14 @@ final class DuckPlayerPreferences: ObservableObject { duckPlayerContingencyHandler.shouldDisplayContingencyMessage } + func reset() { + youtubeOverlayAnyButtonPressed = false + youtubeOverlayInteracted = false + duckPlayerMode = .alwaysAsk + duckPlayerOpenInNewTab = true + duckPlayerAutoplay = true + } + @MainActor func openLearnMoreContingencyURL() { guard let url = duckPlayerContingencyHandler.learnMoreURL else { return } diff --git a/DuckDuckGo/Tab/Model/Tab+Navigation.swift b/DuckDuckGo/Tab/Model/Tab+Navigation.swift index ab3fc72e87..1a4bce2d59 100644 --- a/DuckDuckGo/Tab/Model/Tab+Navigation.swift +++ b/DuckDuckGo/Tab/Model/Tab+Navigation.swift @@ -54,6 +54,9 @@ extension Tab: NavigationResponder { // Duck Player overlay navigations handling .weak(nullable: self.duckPlayer), + // Duck Player onboarding banner + .weak(nullable: self.duckPlayerOnboarding), + // open external scheme link in another app .weak(nullable: self.externalAppSchemeHandler), diff --git a/DuckDuckGo/Tab/TabExtensions/DuckPlayerOnboardingTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DuckPlayerOnboardingTabExtension.swift new file mode 100644 index 0000000000..9c428dec5e --- /dev/null +++ b/DuckDuckGo/Tab/TabExtensions/DuckPlayerOnboardingTabExtension.swift @@ -0,0 +1,76 @@ +// +// DuckPlayerOnboardingTabExtension.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 Navigation +import Combine + +typealias DuckPlayerOnboardingPublisher = AnyPublisher + +final class DuckPlayerOnboardingTabExtension: TabExtension { + @Published private(set) var onboardingState: OnboardingState? + private let onboardingDecider: DuckPlayerOnboardingDecider + + init(onboardingDecider: DuckPlayerOnboardingDecider) { + self.onboardingDecider = onboardingDecider + } +} + +extension DuckPlayerOnboardingTabExtension: NavigationResponder { + + func navigationDidFinish(_ navigation: Navigation) { + guard onboardingDecider.canDisplayOnboarding else { return } + + let locationValidator = DuckPlayerOnboardingLocationValidator() + + Task { @MainActor in + if let webView = navigation.navigationAction.targetFrame?.webView, + await locationValidator.isValidLocation(webView) { + onboardingState = .init(onboardingDecider: onboardingDecider) + } + } + } +} + +struct OnboardingState { + let onboardingDecider: DuckPlayerOnboardingDecider +} + +protocol DuckPlayerOnboardingProtocol: AnyObject, NavigationResponder { + var duckPlayerOnboardingPublisher: DuckPlayerOnboardingPublisher { get } +} + +extension DuckPlayerOnboardingTabExtension: DuckPlayerOnboardingProtocol { + func getPublicProtocol() -> DuckPlayerOnboardingProtocol { self } + + var duckPlayerOnboardingPublisher: DuckPlayerOnboardingPublisher { + self.$onboardingState.eraseToAnyPublisher() + } +} + +extension TabExtensions { + var duckPlayerOnboarding: DuckPlayerOnboardingProtocol? { + resolve(DuckPlayerOnboardingTabExtension.self) + } +} + +extension Tab { + var duckPlayerOnboardingPublisher: DuckPlayerOnboardingPublisher { + self.duckPlayerOnboarding?.duckPlayerOnboardingPublisher ?? Just(nil).eraseToAnyPublisher() + } +} diff --git a/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift index 92d5a63573..21f8dc1cc2 100644 --- a/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift @@ -53,17 +53,19 @@ final class DuckPlayerTabExtension { } private weak var youtubeOverlayScript: YoutubeOverlayUserScript? private weak var youtubePlayerScript: YoutubePlayerUserScript? - + private let onboardingDecider: DuckPlayerOnboardingDecider private var shouldSelectNextNewTab: Bool? init(duckPlayer: DuckPlayer, isBurner: Bool, scriptsPublisher: some Publisher, webViewPublisher: some Publisher, - preferences: DuckPlayerPreferences = .shared) { + preferences: DuckPlayerPreferences = .shared, + onboardingDecider: DuckPlayerOnboardingDecider) { self.duckPlayer = duckPlayer self.isBurner = isBurner self.preferences = preferences + self.onboardingDecider = onboardingDecider webViewPublisher.sink { [weak self] webView in self?.webView = webView @@ -87,6 +89,12 @@ final class DuckPlayerTabExtension { youtubePlayerCancellables.removeAll() guard duckPlayer.isAvailable else { return } + onboardingDecider.valueChangedPublisher.sink {[weak self] _ in + guard let self = self else { return } + + self.youtubeOverlayScript?.userUISettingsUpdated(uiValues: UIUserValues(onboardingDecider: self.onboardingDecider)) + }.store(in: &youtubePlayerCancellables) + if let hostname = url?.host, let script = youtubeOverlayScript { if script.messageOriginPolicy.isAllowed(hostname) { duckPlayer.$mode @@ -176,6 +184,7 @@ extension DuckPlayerTabExtension: NavigationResponder { @MainActor func decidePolicy(for navigationAction: NavigationAction, preferences: inout NavigationPreferences) async -> NavigationActionPolicy? { // only proceed when Private Player is enabled + guard duckPlayer.isAvailable, duckPlayer.mode != .disabled else { return decidePolicyWithDisabledDuckPlayer(for: navigationAction) } @@ -254,7 +263,7 @@ extension DuckPlayerTabExtension: NavigationResponder { func navigation(_ navigation: Navigation, didSameDocumentNavigationOf navigationType: WKSameDocumentNavigationType) { // Navigating to a Youtube URL without page reload - if duckPlayer.mode == .enabled, + if shouldOpenDuckPlayerDirectly, case .sessionStatePush = navigationType, let webView, let url = webView.url, url.isYoutubeVideo, @@ -295,7 +304,7 @@ extension DuckPlayerTabExtension: NavigationResponder { // SERP+Video <<<< YT (redirected to DP) <- Duck Player // if case .backForward(distance: let distance) = navigationAction.navigationType, distance < 0, - duckPlayer.mode == .enabled, + shouldOpenDuckPlayerDirectly, navigationAction.sourceFrame.url.isDuckPlayer, navigationAction.url.youtubeVideoID == navigationAction.sourceFrame.url.youtubeVideoID, let mainFrame = navigationAction.mainFrameTarget { @@ -319,7 +328,7 @@ extension DuckPlayerTabExtension: NavigationResponder { } // Redirect youtube urls to Duck Player when [Always enable] preference is set - if duckPlayer.mode == .enabled + if shouldOpenDuckPlayerDirectly // - or - recommendations must always be opened in the Duck Player || (navigationAction.sourceFrame.url.isDuckPlayer && navigationAction.url.isYoutubeVideoRecommendation), let mainFrame = navigationAction.mainFrameTarget { @@ -353,7 +362,7 @@ extension DuckPlayerTabExtension: NavigationResponder { return } if navigation.url.isDuckPlayer { - let setting = duckPlayer.mode == .enabled ? "always" : "default" + var setting = preferences.duckPlayerMode == .enabled ? "always" : "default" let newTabSettings = preferences.duckPlayerOpenInNewTab ? "true" : "false" let autoplay = preferences.duckPlayerAutoplay ? "true" : "false" @@ -380,5 +389,7 @@ extension DuckPlayerTabExtension: DuckPlayerExtensionProtocol, TabExtension { } extension TabExtensions { - var duckPlayer: DuckPlayerExtensionProtocol? { resolve(DuckPlayerTabExtension.self) } + var duckPlayer: DuckPlayerExtensionProtocol? { + resolve(DuckPlayerTabExtension.self) + } } diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index e998c25902..42eb48da2c 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -188,11 +188,17 @@ extension TabExtensionsBuilder { NavigationHotkeyHandler(isTabPinned: args.isTabPinned, isBurner: args.isTabBurner) } + let duckPlayerOnboardingDecider = DefaultDuckPlayerOnboardingDecider() add { DuckPlayerTabExtension(duckPlayer: dependencies.duckPlayer, isBurner: args.isTabBurner, scriptsPublisher: userScripts.compactMap { $0 }, - webViewPublisher: args.webViewFuture) + webViewPublisher: args.webViewFuture, + onboardingDecider: duckPlayerOnboardingDecider) + } + + add { + DuckPlayerOnboardingTabExtension(onboardingDecider: duckPlayerOnboardingDecider) } add { diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 051665ebde..0d1f226caf 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -44,6 +44,7 @@ final class BrowserTabViewController: NSViewController { private var tabViewModelCancellables = Set() private var activeUserDialogCancellable: Cancellable? + private var duckPlayerConsentCancellable: AnyCancellable? private var pinnedTabsDelegatesCancellable: AnyCancellable? private var keyWindowSelectedTabCancellable: AnyCancellable? private var cancellables = Set() @@ -54,6 +55,10 @@ final class BrowserTabViewController: NSViewController { private var hoverLabelWorkItem: DispatchWorkItem? private(set) var transientTabContentViewController: NSViewController? + private lazy var duckPlayerOnboardingModalManager: DuckPlayerOnboardingModalManager = { + let modal = DuckPlayerOnboardingModalManager() + return modal + }() required init?(coder: NSCoder) { fatalError("BrowserTabViewController: Bad initializer") @@ -255,6 +260,7 @@ final class BrowserTabViewController: NSViewController { self.subscribeToTabContent(of: selectedTabViewModel) self.subscribeToHoveredLink(of: selectedTabViewModel) self.subscribeToUserDialogs(of: selectedTabViewModel) + self.subscribeToDuckPlayerOnboardingPrompt(of: selectedTabViewModel) self.adjustFirstResponder(force: true) } @@ -430,6 +436,18 @@ final class BrowserTabViewController: NSViewController { #endif } + private func subscribeToDuckPlayerOnboardingPrompt(of tabViewModel: TabViewModel?) { + tabViewModel?.tab.duckPlayerOnboardingPublisher.sink { [weak self, weak tab = tabViewModel?.tab] onboardingState in + + guard let self, let tab, let onboardingState = onboardingState, onboardingState.onboardingDecider.canDisplayOnboarding else { + self?.duckPlayerOnboardingModalManager.close(animated: false, completion: nil) + return + } + + self.duckPlayerOnboardingModalManager.show(on: self.view, animated: true) + }.store(in: &tabViewModelCancellables) + } + private func shouldMakeContentViewFirstResponder(for tabContent: Tab.TabContent) -> Bool { // always steal focus when first responder is not a text field guard view.window?.firstResponder is NSText else { diff --git a/DuckDuckGo/YoutubePlayer/DuckPlayer.swift b/DuckDuckGo/YoutubePlayer/DuckPlayer.swift index 1890a232fe..bc26df30dd 100644 --- a/DuckDuckGo/YoutubePlayer/DuckPlayer.swift +++ b/DuckDuckGo/YoutubePlayer/DuckPlayer.swift @@ -57,6 +57,7 @@ struct InitialPlayerSettings: Codable { struct PlayerSettings: Codable { let pip: PIP let autoplay: Autoplay + let focusMode: FocusMode } struct PIP: Codable { @@ -75,6 +76,17 @@ struct InitialPlayerSettings: Codable { let state: State } + /// Represents the current focus mode of the player. + /// + /// Focus mode determines whether the bottom toolbar should be visible or hidden. + /// When focus mode is enabled, the toolbar will auto-hide after a few seconds. + /// When focus mode is disabled, the toolbar will always be visible and the background wallpaper will be slightly brighter. + /// + /// Default should be enabled. + struct FocusMode: Codable { + let state: State + } + enum State: String, Codable { case enabled case disabled @@ -90,10 +102,12 @@ struct InitialPlayerSettings: Codable { let platform: Platform let environment: Environment let locale: Locale + } struct InitialOverlaySettings: Codable { let userValues: UserValues + let ui: UIUserValues } // Values that the YouTube Overlays can use to determine the current state @@ -111,6 +125,15 @@ public struct UserValues: Codable { let overlayInteracted: Bool } +public struct UIUserValues: Codable { + /// If this value is true, we force the FE layer to play in duck player even if the settings is off + let playInDuckPlayer: Bool + + init(onboardingDecider: DuckPlayerOnboardingDecider) { + self.playInDuckPlayer = onboardingDecider.shouldOpenFirstVideoOnDuckPlayer + } +} + final class DuckPlayer { static let usesSimulatedRequests: Bool = { if #available(macOS 12.0, *) { @@ -145,12 +168,14 @@ final class DuckPlayer { init( preferences: DuckPlayerPreferences = .shared, - privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager + privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, + onboardingDecider: DuckPlayerOnboardingDecider = DefaultDuckPlayerOnboardingDecider() ) { self.preferences = preferences isFeatureEnabled = privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .duckPlayer) isPiPFeatureEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DuckPlayerSubfeature.pip) isAutoplayFeatureEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(DuckPlayerSubfeature.autoplay) + self.onboardingDecider = onboardingDecider mode = preferences.duckPlayerMode bindDuckPlayerModeIfNeeded() @@ -265,9 +290,13 @@ final class DuckPlayer { let platform = InitialPlayerSettings.Platform(name: "macos") let environment = InitialPlayerSettings.Environment.development let locale = InitialPlayerSettings.Locale.en - let playerSettings = InitialPlayerSettings.PlayerSettings(pip: pip, autoplay: autoplay) + let focusMode = InitialPlayerSettings.FocusMode(state: onboardingDecider.shouldOpenFirstVideoOnDuckPlayer ? .disabled : .enabled) + let playerSettings = InitialPlayerSettings.PlayerSettings(pip: pip, autoplay: autoplay, focusMode: focusMode) let userValues = encodeUserValues() + /// Since the FE is requesting player-encoded values, we can assume that the first player video setup is complete from the onboarding point of view. + onboardingDecider.setFirstVideoInDuckPlayerAsDone() + return InitialPlayerSettings(userValues: userValues, settings: playerSettings, platform: platform, @@ -279,7 +308,7 @@ final class DuckPlayer { private func encodedOverlaySettings(with webView: WKWebView?) async -> InitialOverlaySettings { let userValues = encodeUserValues() - return InitialOverlaySettings(userValues: userValues) + return InitialOverlaySettings(userValues: userValues, ui: UIUserValues(onboardingDecider: onboardingDecider)) } // MARK: - Private @@ -296,6 +325,7 @@ final class DuckPlayer { private var isFeatureEnabledCancellable: AnyCancellable? private var isPiPFeatureEnabled: Bool private var isAutoplayFeatureEnabled: Bool + private let onboardingDecider: DuckPlayerOnboardingDecider private func bindDuckPlayerModeIfNeeded() { if isFeatureEnabled { diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingLocationValidator.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingLocationValidator.swift new file mode 100644 index 0000000000..1b467c9f04 --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingLocationValidator.swift @@ -0,0 +1,52 @@ +// +// DuckPlayerOnboardingLocationValidator.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 Navigation + +struct DuckPlayerOnboardingLocationValidator { + private static let youtubeChannelCheckScript = """ + (function() { + var canonicalLink = document.querySelector('link[rel="canonical"]'); + return canonicalLink && canonicalLink.href.includes('channel'); + })(); + """ + + func isValidLocation(_ webView: WKWebView) async -> Bool { + guard let url = await webView.url else { return false } + + let isRootURL = isYoutubeRootURL(url) + let isInChannel = await isCurrentWebViewInAYoutubeChannel(webView) + return isRootURL || isInChannel + } + + private func isYoutubeRootURL(_ url: URL) -> Bool { + guard let urlComponents = URLComponents(string: url.absoluteString) else { return false } + return urlComponents.scheme == "https" && + urlComponents.host == "www.youtube.com" && + urlComponents.path == "/" + } + + private func isCurrentWebViewInAYoutubeChannel(_ webView: WKWebView) async -> Bool { + do { + return try await webView.evaluateJavaScript(DuckPlayerOnboardingLocationValidator.youtubeChannelCheckScript) as Bool? ?? false + } catch { + return false + } + } +} diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingDecider.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingDecider.swift new file mode 100644 index 0000000000..d76955b560 --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingDecider.swift @@ -0,0 +1,143 @@ +// +// DuckPlayerOnboardingDecider.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 + +/// A protocol for deciding whether to display onboarding and open the first video in the Duck Player. +protocol DuckPlayerOnboardingDecider { + /// A boolean indicating whether the onboarding should be displayed. + var canDisplayOnboarding: Bool { get } + + /// A boolean indicating whether the first video should be opened in the Duck Player. + var shouldOpenFirstVideoOnDuckPlayer: Bool { get } + + /// Sets the onboarding as done. + /// + /// This method should be called when the onboarding has been completed. + func setOnboardingAsDone() + + /// Sets the flag to open the first video in the Duck Player. + /// + /// This method should be called when user selects to use Duck Player during the onboarding + func setOpenFirstVideoOnDuckPlayer() + + /// Sets the first video in the Duck Player as done. + /// + /// This method should be called when the first video has been opened in the Duck Player. + func setFirstVideoInDuckPlayerAsDone() + + /// A publisher that emits a notification whenever any onboarding or video flags change. + /// + /// Subscribe to receive updates when `canDisplayOnboarding`, `shouldOpenFirstVideoOnDuckPlayer`, or related values change. + /// + /// Note that this publisher will emit a notification whenever any of the underlying values change, even if the change is made on a different instance of `DefaultDuckPlayerOnboardingDecider`. + var valueChangedPublisher: PassthroughSubject { get set } + + /// Resets the onboarding and video flags to their initial state. + /// + /// This method should be called when the onboarding and video flags need to be reset. + func reset() +} + +import Combine + +struct DefaultDuckPlayerOnboardingDecider: DuckPlayerOnboardingDecider { + private let defaults: UserDefaults + private var observer: NSObjectProtocol? + private let preferences: DuckPlayerPreferences + var valueChangedPublisher: PassthroughSubject = .init() + + init(defaults: UserDefaults = .standard, preferences: DuckPlayerPreferences = .shared) { + self.defaults = defaults + self.preferences = preferences + observer = NotificationCenter.default.addObserver(forName: .valuesDidChange, object: nil, queue: nil) { [valuesDidChange] _ in + valuesDidChange() + } + } + + /// We only want to display the onboarding if it was never displayed, the settings is set to alwaysAsk and haven't interacted with the overlay. + var canDisplayOnboarding: Bool { +#if DEBUG + return !defaults.onboardingWasDisplayed && preferences.duckPlayerMode == .alwaysAsk +#else + // returning false until we turn on the experiment + return false +#endif + + } + + private var isUserInExperiment: Bool { + return false + } + + var shouldOpenFirstVideoOnDuckPlayer: Bool { + return defaults.shouldOpenFirstVideoInDuckPlayer && !defaults.firstVideoWasOpenedInDuckPlayer + } + + func setOnboardingAsDone() { + NotificationCenter.default.post(name: .valuesDidChange, object: nil) + defaults.onboardingWasDisplayed = true + } + + func setOpenFirstVideoOnDuckPlayer() { + defaults.shouldOpenFirstVideoInDuckPlayer = true + NotificationCenter.default.post(name: .valuesDidChange, object: nil) + } + + func setFirstVideoInDuckPlayerAsDone() { + defaults.firstVideoWasOpenedInDuckPlayer = true + NotificationCenter.default.post(name: .valuesDidChange, object: nil) + } + + func reset() { + defaults.onboardingWasDisplayed = false + defaults.shouldOpenFirstVideoInDuckPlayer = false + defaults.firstVideoWasOpenedInDuckPlayer = false + } + + private func valuesDidChange() { + valueChangedPublisher.send() + } +} + +private extension UserDefaults { + enum Keys { + static let onboardingWasDisplayed = "duckplayer.onboarding-displayed" + static let firstVideoWasOpenedInDuckPlayer = "duckplayer.onboarding.first-video-opened" + static let shouldOpenFirstVideoInDuckPlayer = "duckplayer.onboarding.should-open-in-duckplayer" + } + + var onboardingWasDisplayed: Bool { + get { return bool(forKey: Keys.onboardingWasDisplayed) } + set { set(newValue, forKey: Keys.onboardingWasDisplayed) } + } + + var firstVideoWasOpenedInDuckPlayer: Bool { + get { return bool(forKey: Keys.firstVideoWasOpenedInDuckPlayer) } + set { set(newValue, forKey: Keys.firstVideoWasOpenedInDuckPlayer) } + } + + var shouldOpenFirstVideoInDuckPlayer: Bool { + get { return bool(forKey: Keys.shouldOpenFirstVideoInDuckPlayer) } + set { set(newValue, forKey: Keys.shouldOpenFirstVideoInDuckPlayer) } + } +} + +private extension Notification.Name { + static let valuesDidChange = Notification.Name("duckplayer.onboarding.should-open-first-video") +} diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalManager.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalManager.swift new file mode 100644 index 0000000000..63e56e7c06 --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalManager.swift @@ -0,0 +1,29 @@ +// +// DuckPlayerOnboardingModalManager.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 + +final class DuckPlayerOnboardingModalManager: TabModalManageable { + var modal: TabModal? + + var viewController: NSViewController { + DuckPlayerOnboardingViewController { [weak self] in + self?.close(animated: true, completion: nil) + } + } +} diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalView.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalView.swift new file mode 100644 index 0000000000..8f7ecbae99 --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingModalView.swift @@ -0,0 +1,293 @@ +// +// DuckPlayerOnboardingModalView.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 SwiftUI + +struct DuckPlayerOnboardingModalView: View { + private enum Constants { + static let outerContainerWidth: CGFloat = 504 + static let smallContainerHeight: CGFloat = 182 + static let bigContainerHeight: CGFloat = 286 + static let containerCornerRadius: CGFloat = 12 + static let darkModeBorderColor: Color = .white.opacity(0.2) + static let whiteModeBorderColor: Color = .black.opacity(0.1) + } + + @ObservedObject var viewModel: DuckPlayerOnboardingViewModel + @Environment(\.colorScheme) var colorScheme + + var body: some View { + currentView + .padding() + .frame(width: Constants.outerContainerWidth, height: containerHeight) + .padding(.horizontal) + .background(Color("DialogPanelBackground")) + .cornerRadius(Constants.containerCornerRadius) + .overlay( + RoundedRectangle(cornerRadius: Constants.containerCornerRadius) + .stroke(colorScheme == .dark ? Constants.darkModeBorderColor : Constants.whiteModeBorderColor, lineWidth: 1) + ) + } + + private var containerHeight: CGFloat { + switch viewModel.currentView { + case .confirmation: + return Constants.smallContainerHeight + + case .onboardingOptions: + return Constants.bigContainerHeight + } + } + + @ViewBuilder + var currentView: some View { + switch viewModel.currentView { + case .confirmation: + DuckPlayerOnboardingConfirmationView { + viewModel.handleGotItCTA() + } + + case .onboardingOptions: + DuckPlayerOnboardingChoiceView(turnOnButtonPressed: { + viewModel.currentView = .confirmation + viewModel.handleTurnOnCTA() + }, notNowPressed: viewModel.handleNotNowCTA) + } + } +} + +private struct DuckPlayerOnboardingChoiceView: View { + let turnOnButtonPressed: () -> Void + let notNowPressed: () -> Void + + var body: some View { + VStack(spacing: 20) { + DaxSpeechBubble { + VStack (alignment: .leading, spacing: 8) { + Text(UserText.duckPlayerOnboardingChoiceModalTitle) + .font(.title) + .padding(.horizontal) + + Text(UserText.duckPlayerOnboardingChoiceModalMessage) + .font(.body) + .multilineText() + .padding(.horizontal) + + HStack { + Spacer() + Image("DuckPlayerOnboardingModal") + Spacer() + } + }.frame(maxWidth: .infinity) + .padding() + + } + + HStack { + Button { + notNowPressed() + } label: { + Text(UserText.duckPlayerOnboardingChoiceModalCTADeny) + } + .buttonStyle(SecondaryCTAStyle()) + + Spacer() + Button { + turnOnButtonPressed() + } label: { + Text(UserText.duckPlayerOnboardingChoiceModalCTAConfirm) + } + .buttonStyle(PrimaryCTAStyle()) + } + } + } +} + +private struct DuckPlayerOnboardingConfirmationView: View { + let voidButtonPressed: () -> Void + var body: some View { + VStack(spacing: 20) { + DaxSpeechBubble { + VStack(alignment: .leading, spacing: 8) { + Text(UserText.duckPlayerOnboardingConfirmationModalTitle) + .font(.title) + .padding(.horizontal) + + Text(UserText.duckPlayerOnboardingConfirmationModalMessage) + .font(.body) + .padding(.horizontal) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + + } + + Button { + voidButtonPressed() + } label: { + Text(UserText.duckPlayerOnboardingConfirmationModalCTAConfirm) + } + .buttonStyle(PrimaryCTAStyle()) + } + } +} + +private struct DaxSpeechBubble: View { + let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + VStack { + HStack(alignment: .top, spacing: 12) { + Image("DuckPlayerOnboardingModalDax") + .padding(.leading, -10) + + ZStack { + SpeechBubble() + content + } + } + } + } +} + +private struct SpeechBubble: View { + let radius: CGFloat = 20 + let tailSize: CGFloat = 12 + let tailPosition: CGFloat = 38 + let tailHeight: CGFloat = 28 + + var body: some View { + ZStack { + GeometryReader { g in + let rect = CGRect(x: 0, y: 0, width: g.size.width, height: g.size.height) + + Path { path in + + path.move(to: CGPoint(x: rect.minX, y: rect.maxY - radius)) + + path.addLine(to: CGPoint(x: rect.minX, y: tailPosition + tailHeight / 2)) + path.addLine(to: CGPoint(x: rect.minX - tailSize, y: tailPosition)) + path.addLine(to: CGPoint(x: rect.minX, y: tailPosition - tailHeight / 2)) + + path.addArc( + center: CGPoint(x: rect.minX + radius, y: rect.minY + radius), + radius: radius, + startAngle: .degrees(180), + endAngle: .degrees(270), + clockwise: false + ) + path.addArc( + center: CGPoint(x: rect.maxX - radius, y: rect.minY + radius), + radius: radius, + startAngle: .degrees(270), + endAngle: .degrees(0), + clockwise: false + ) + path.addArc( + center: CGPoint(x: rect.maxX - radius, y: rect.maxY - radius), + radius: radius, + startAngle: .degrees(0), + endAngle: .degrees(90), + clockwise: false + ) + path.addArc( + center: CGPoint(x: rect.minX + radius, y: rect.maxY - radius), + radius: radius, + startAngle: .degrees(90), + endAngle: .degrees(180), + clockwise: false + ) + + } + .fill(Color(.interfaceBackground)) + .shadow(color: Color(.onboardingDaxSpeechShadow), radius: 2, x: 0, y: 0) + } + + } + } +} + +private enum CTAConstants { + static let CTACornerRadius: CGFloat = 8 +} + +private struct PrimaryCTAStyle: ButtonStyle { + + func makeBody(configuration: Self.Configuration) -> some View { + + let color = configuration.isPressed ? Color("DuckPlayerOnboardingPrimaryButtonPressed") : Color("DuckPlayerOnboardingPrimaryButton") + + configuration.label + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + .truncationMode(.tail) + .background(RoundedRectangle(cornerRadius: CTAConstants.CTACornerRadius, style: .continuous).fill(color)) + .foregroundColor(.white) + .font(.system(size: 13, weight: .light, design: .default)) + } +} + +private struct SecondaryCTAStyle: ButtonStyle { + @Environment(\.colorScheme) var colorScheme + + func makeBody(configuration: Self.Configuration) -> some View { + + let color = configuration.isPressed ? Color("DuckPlayerOnboardingSecondaryButtonPressed") : Color("DuckPlayerOnboardingSecondaryButton") + + let outterShadowOpacity = colorScheme == .dark ? 0.8 : 0.0 + + configuration.label + .font(.system(size: 13, weight: .light, design: .default)) + .foregroundColor(.primary) + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: CTAConstants.CTACornerRadius, style: .continuous) + .fill(color) + .shadow(color: .black.opacity(0.1), radius: 0.1, x: 0, y: 1) + .shadow(color: .primary.opacity(outterShadowOpacity), radius: 0.1, x: 0, y: -0.6)) + + .overlay( + RoundedRectangle(cornerRadius: CTAConstants.CTACornerRadius) + .stroke(Color.black.opacity(0.1), lineWidth: 1)) + } +} + +#Preview { + VStack { + DuckPlayerOnboardingChoiceView(turnOnButtonPressed: { + + }, notNowPressed: { + + }) + .frame(width: 504, height: 286) + + Divider() + .padding() + + DuckPlayerOnboardingConfirmationView(voidButtonPressed: { + + }) + .frame(width: 504, height: 152) + } + .padding() +} diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewController.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewController.swift new file mode 100644 index 0000000000..ff9e00ba07 --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewController.swift @@ -0,0 +1,72 @@ +// +// DuckPlayerOnboardingViewController.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 SwiftUI +import AppKit + +final class DuckPlayerOnboardingViewController: NSViewController { + var didFinish: () -> Void + + internal init(didFinish: @escaping () -> Void) { + self.didFinish = didFinish + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var hostingView: NSHostingView! + private lazy var viewModel: DuckPlayerOnboardingViewModel = { + let viewModel = DuckPlayerOnboardingViewModel() + viewModel.delegate = self + return viewModel + }() + + override func loadView() { + let consentView = DuckPlayerOnboardingModalView(viewModel: viewModel) + hostingView = NSHostingView(rootView: consentView) + self.view = hostingView + } + + private func handleEnableDuckPlayerActionButton() { + print("Enabled") + } + + private func handleNotNowActionButton() { + didFinish() + } + + private func handleGotItActionButton() { + didFinish() + } +} + +extension DuckPlayerOnboardingViewController: DuckPlayerOnboardingViewModelDelegate{ + func duckPlayerOnboardingViewModelDidSelectTurnOn(_ viewModel: DuckPlayerOnboardingViewModel) { + handleEnableDuckPlayerActionButton() + } + + func duckPlayerOnboardingViewModelDidSelectNotNow(_ viewModel: DuckPlayerOnboardingViewModel) { + handleNotNowActionButton() + } + + func duckPlayerOnboardingViewModelDidSelectGotIt(_ viewModel: DuckPlayerOnboardingViewModel) { + handleGotItActionButton() + } +} diff --git a/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewModel.swift b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewModel.swift new file mode 100644 index 0000000000..d5996ae3ca --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/Onboarding/DuckPlayerOnboardingModal/DuckPlayerOnboardingViewModel.swift @@ -0,0 +1,57 @@ +// +// DuckPlayerOnboardingViewModel.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 DuckPlayerOnboardingViewModelDelegate: AnyObject { + func duckPlayerOnboardingViewModelDidSelectTurnOn(_ viewModel: DuckPlayerOnboardingViewModel) + func duckPlayerOnboardingViewModelDidSelectNotNow(_ viewModel: DuckPlayerOnboardingViewModel) + func duckPlayerOnboardingViewModelDidSelectGotIt(_ viewModel: DuckPlayerOnboardingViewModel) +} + +final class DuckPlayerOnboardingViewModel: ObservableObject { + private let onboardingDecider: DuckPlayerOnboardingDecider + + enum DuckPlayerModalCurrentView { + case onboardingOptions + case confirmation + } + + init(onboardingDecider: DuckPlayerOnboardingDecider = DefaultDuckPlayerOnboardingDecider()) { + self.onboardingDecider = onboardingDecider + } + + @Published var currentView: DuckPlayerModalCurrentView = .onboardingOptions + weak var delegate: DuckPlayerOnboardingViewModelDelegate? + + func handleTurnOnCTA() { + delegate?.duckPlayerOnboardingViewModelDidSelectTurnOn(self) + + onboardingDecider.setOpenFirstVideoOnDuckPlayer() + onboardingDecider.setOnboardingAsDone() + } + + func handleNotNowCTA() { + delegate?.duckPlayerOnboardingViewModelDidSelectNotNow(self) + onboardingDecider.setOnboardingAsDone() + } + + func handleGotItCTA() { + delegate?.duckPlayerOnboardingViewModelDidSelectGotIt(self) + } +} diff --git a/DuckDuckGo/YoutubePlayer/TabModal/TabModal.swift b/DuckDuckGo/YoutubePlayer/TabModal/TabModal.swift new file mode 100644 index 0000000000..1c6422ba8e --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/TabModal/TabModal.swift @@ -0,0 +1,140 @@ +// +// TabModal.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 Cocoa +import Combine +private enum AnimationConsts { + static let yAnimationOffset: CGFloat = 65 + static let duration: CGFloat = 0.6 +} + +public final class TabModal { + private let modalViewController: NSViewController + private lazy var windowController: NSWindowController = { + let windowController = NSWindowController(window: NSWindow(contentViewController: modalViewController)) + + if let window = windowController.window { + window.styleMask = [.borderless] + window.acceptsMouseMovedEvents = true + window.ignoresMouseEvents = false + window.backgroundColor = .clear + window.isOpaque = false + window.hasShadow = true + window.level = .floating + } + modalViewController.view.wantsLayer = true + return windowController + }() + + private var resizeObserver: Any? + private var cancellables = Set() + + public init(modalViewController: NSViewController) { + self.modalViewController = modalViewController + } + + public required init?(coder: NSCoder) { + fatalError("OnboardingModal: Bad initializer") + } + + // MARK: - Private methods + + private func windowDidResize(_ parent: NSWindow) { + guard let overlayWindow = windowController.window else { + return + } + + let xPosition = (parent.frame.width / 2) - (overlayWindow.frame.width / 2) + parent.frame.origin.x + let yPosition = parent.frame.origin.y + parent.frame.height - overlayWindow.frame.height - AnimationConsts.yAnimationOffset + + let size = overlayWindow.frame.size + let newOrigin = NSPoint(x: xPosition, y: yPosition) + overlayWindow.setFrame(NSRect(origin: newOrigin, size: size), display: true) + } + + private func addObserverForWindowResize(_ window: NSWindow) { + NotificationCenter.default.publisher(for: NSWindow.didResizeNotification, object: window) + .receive(on: DispatchQueue.main) + .sink { [weak self] notification in + guard let parent = notification.object as? NSWindow else { return } + self?.windowDidResize(parent) + } + .store(in: &cancellables) + } +} + +// MARK: - Public methods +extension TabModal: TabModalPresentable { + public func close(animated: Bool, completion: (() -> Void)? = nil) { + guard let overlayWindow = windowController.window else { + return + } + if !overlayWindow.isVisible { return } + + let removeWindow = { + overlayWindow.parent?.removeChildWindow(overlayWindow) + overlayWindow.orderOut(nil) + completion?() + } + + if animated { + NSAnimationContext.runAnimationGroup { context in + context.duration = AnimationConsts.duration + + let newOrigin = NSPoint(x: overlayWindow.frame.origin.x, y: overlayWindow.frame.origin.y + AnimationConsts.yAnimationOffset) + let size = overlayWindow.frame.size + overlayWindow.animator().alphaValue = 0 + overlayWindow.animator().setFrame(NSRect(origin: newOrigin, size: size), display: true) + } completionHandler: { + removeWindow() + } + } else { + removeWindow() + } + } + + public func show(on currentTabView: NSView, animated: Bool) { + guard let currentTabViewWindow = currentTabView.window, + let overlayWindow = windowController.window else { + return + } + + addObserverForWindowResize(currentTabViewWindow) + + currentTabViewWindow.addChildWindow(overlayWindow, ordered: .above) + + let xPosition = (currentTabViewWindow.frame.width / 2) - (overlayWindow.frame.width / 2) + currentTabViewWindow.frame.origin.x + let yPosition = currentTabViewWindow.frame.origin.y + currentTabViewWindow.frame.height - overlayWindow.frame.height + + if animated { + overlayWindow.setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) + overlayWindow.alphaValue = 0 + + NSAnimationContext.runAnimationGroup { context in + context.duration = AnimationConsts.duration + let newOrigin = NSPoint(x: xPosition, y: yPosition - AnimationConsts.yAnimationOffset) + let size = overlayWindow.frame.size + overlayWindow.animator().alphaValue = 1 + overlayWindow.animator().setFrame(NSRect(origin: newOrigin, size: size), display: true) + + } + } else { + overlayWindow.setFrameOrigin(NSPoint(x: xPosition, y: yPosition - AnimationConsts.yAnimationOffset)) + } + } +} diff --git a/DuckDuckGo/YoutubePlayer/TabModal/TabModalManageable.swift b/DuckDuckGo/YoutubePlayer/TabModal/TabModalManageable.swift new file mode 100644 index 0000000000..7da40e235f --- /dev/null +++ b/DuckDuckGo/YoutubePlayer/TabModal/TabModalManageable.swift @@ -0,0 +1,82 @@ +// +// TabModalManageable.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 + +/// A protocol for handling the view controller to be displayed in the modal. +protocol TabModalPresentable: AnyObject { + /// Initializes a new instance of the modal presenter with the given view controller. + /// + /// - Parameter modalViewController: The view controller to be presented in the modal. + init(modalViewController: NSViewController) + + /// Closes the modal view controller. + /// + /// - Parameters: + /// - animated: A boolean indicating whether the closure of the modal should be animated. + /// - completion: An optional closure to be executed after the modal has been closed. + func close(animated: Bool, completion: (() -> Void)?) + + /// Shows the modal view controller on the given view. + /// + /// - Parameters: + /// - currentTabView: The view on which the modal should be presented. + /// - animated: A boolean indicating whether the presentation of the modal should be animated. + func show(on currentTabView: NSView, animated: Bool) +} + +/// A protocol for managing the modal to be presented. +protocol TabModalManageable: AnyObject { + associatedtype ModalType: TabModalPresentable + + var modal: ModalType? { get set } + var viewController: NSViewController { get } + + /// Closes the modal view controller. + /// + /// - Parameters: + /// - animated: A boolean indicating whether the closure of the modal should be animated. + /// - completion: An optional closure to be executed after the modal has been closed. + func close(animated: Bool, completion: (() -> Void)?) + + /// Shows the modal view controller on the given view. + /// + /// - Parameters: + /// - currentTabView: The view on which the modal should be presented. + /// - animated: A boolean indicating whether the presentation of the modal should be animated. + func show(on currentTabView: NSView, animated: Bool) +} + +extension TabModalManageable { + + func close(animated: Bool, completion: (() -> Void)?) { + modal?.close(animated: animated) { [weak self] in + self?.modal = nil + } + } + + func show(on currentTabView: NSView, animated: Bool) { + prepareModal() + modal?.show(on: currentTabView, animated: animated) + } + + private func prepareModal() { + guard modal == nil else { return } + modal = ModalType(modalViewController: viewController) + } +} diff --git a/DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift b/DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift index 79ba756c2d..4d20a6ccd4 100644 --- a/DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift +++ b/DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift @@ -21,6 +21,7 @@ import WebKit import Common import UserScript import PixelKit +import Combine protocol YoutubeOverlayUserScriptDelegate: AnyObject { func youtubeOverlayUserScriptDidRequestDuckPlayer(with url: URL, in webView: WKWebView) @@ -95,6 +96,7 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature { } } + // User values are user controled values public func userValuesUpdated(userValues: UserValues) { guard let webView = webView else { return assertionFailure("Could not access webView") @@ -102,6 +104,14 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature { broker?.push(method: "onUserValuesChanged", params: userValues, for: self, into: webView) } + // Temporary changes to user settings + public func userUISettingsUpdated(uiValues: UIUserValues) { + guard let webView = webView else { + return assertionFailure("Could not access webView") + } + broker?.push(method: "onUIValuesChanged", params: uiValues, for: self, into: webView) + } + // MARK: - Private Methods @MainActor diff --git a/UnitTests/YoutubePlayer/DefaultDuckPlayerOnboardingDeciderTests.swift b/UnitTests/YoutubePlayer/DefaultDuckPlayerOnboardingDeciderTests.swift new file mode 100644 index 0000000000..7400fb7027 --- /dev/null +++ b/UnitTests/YoutubePlayer/DefaultDuckPlayerOnboardingDeciderTests.swift @@ -0,0 +1,146 @@ +// +// DefaultDuckPlayerOnboardingDeciderTests.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 XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class DefaultDuckPlayerOnboardingDeciderTests: XCTestCase { + + var decider: DefaultDuckPlayerOnboardingDecider! + var defaults: UserDefaults! + static let defaultsName = "TestDefaults" + + override func setUp() { + super.setUp() + defaults = UserDefaults(suiteName: DefaultDuckPlayerOnboardingDeciderTests.defaultsName)! + decider = DefaultDuckPlayerOnboardingDecider(defaults: defaults) + } + + override func tearDown() { + super.tearDown() + defaults.removePersistentDomain(forName: DefaultDuckPlayerOnboardingDeciderTests.defaultsName) + } + + func testCanDisplayOnboarding_InitiallyReturnsTrue() { + XCTAssertTrue(decider.canDisplayOnboarding) + } + + func testCanDisplayOnboarding_ReturnsFalseAfterSettingOnboardingAsDone() { + decider.setOnboardingAsDone() + XCTAssertFalse(decider.canDisplayOnboarding) + } + + func testShouldOpenFirstVideoOnDuckPlayer_InitiallyReturnsFalse() { + XCTAssertFalse(decider.shouldOpenFirstVideoOnDuckPlayer) + } + + func testShouldOpenFirstVideoOnDuckPlayer_ReturnsTrueAfterSettingOpenFirstVideo() { + decider.setOpenFirstVideoOnDuckPlayer() + XCTAssertTrue(decider.shouldOpenFirstVideoOnDuckPlayer) + } + + func testShouldOpenFirstVideoOnDuckPlayer_ReturnsFalseAfterSettingFirstVideoAsDone() { + decider.setOpenFirstVideoOnDuckPlayer() + XCTAssertTrue(decider.shouldOpenFirstVideoOnDuckPlayer) + + decider.setFirstVideoInDuckPlayerAsDone() + XCTAssertFalse(decider.shouldOpenFirstVideoOnDuckPlayer) + } + + func testSetOnboardingAsDone_canDisplayOnboardingReturnsFalse() { + XCTAssertTrue(decider.canDisplayOnboarding) + decider.setOnboardingAsDone() + XCTAssertFalse(decider.canDisplayOnboarding) + } + + func testCanDisplayOnboarding_WhenAlwaysAskAndNotInteracted() { + let preferences = DuckPlayerPreferences( + persistor: DuckPlayerPreferencesPersistorMock( + duckPlayerMode: .alwaysAsk, + youtubeOverlayInteracted: false, + youtubeOverlayAnyButtonPressed: false + ) + ) + + let onboardingDecider = DefaultDuckPlayerOnboardingDecider(defaults: defaults, preferences: preferences) + XCTAssertTrue(onboardingDecider.canDisplayOnboarding) + } + + func testCanDisplayOnboarding_WhenAlwaysAskAndInteracted() { + let preferences = DuckPlayerPreferences( + persistor: DuckPlayerPreferencesPersistorMock( + duckPlayerMode: .alwaysAsk, + youtubeOverlayInteracted: false, + youtubeOverlayAnyButtonPressed: true + ) + ) + + let onboardingDecider = DefaultDuckPlayerOnboardingDecider(defaults: defaults, preferences: preferences) + XCTAssertTrue(onboardingDecider.canDisplayOnboarding) + } + + func testCanDisplayOnboarding_WhenEnabled() { + let preferences = DuckPlayerPreferences( + persistor: DuckPlayerPreferencesPersistorMock( + duckPlayerMode: .enabled, + youtubeOverlayInteracted: false, + youtubeOverlayAnyButtonPressed: false + ) + ) + + let onboardingDecider = DefaultDuckPlayerOnboardingDecider(defaults: defaults, preferences: preferences) + XCTAssertFalse(onboardingDecider.canDisplayOnboarding) + } + + func testCanDisplayOnboarding_WhenDisabled() { + let preferences = DuckPlayerPreferences( + persistor: DuckPlayerPreferencesPersistorMock( + duckPlayerMode: .disabled, + youtubeOverlayInteracted: false, + youtubeOverlayAnyButtonPressed: false + ) + ) + + let onboardingDecider = DefaultDuckPlayerOnboardingDecider(defaults: defaults, preferences: preferences) + XCTAssertFalse(onboardingDecider.canDisplayOnboarding) + } + + func testCanDisplayOnboarding_WhenOnboardingWasDisplayed() { + let preferences = DuckPlayerPreferences( + persistor: DuckPlayerPreferencesPersistorMock( + duckPlayerMode: .alwaysAsk, + youtubeOverlayInteracted: false, + youtubeOverlayAnyButtonPressed: false + ) + ) + + let onboardingDecider = DefaultDuckPlayerOnboardingDecider(defaults: defaults, preferences: preferences) + onboardingDecider.setOnboardingAsDone() + XCTAssertFalse(onboardingDecider.canDisplayOnboarding) + } + + func testReset_ResetsAllFlagsToFalse() { + decider.setOnboardingAsDone() + decider.setOpenFirstVideoOnDuckPlayer() + decider.setFirstVideoInDuckPlayerAsDone() + decider.reset() + + XCTAssertTrue(decider.canDisplayOnboarding) + XCTAssertFalse(decider.shouldOpenFirstVideoOnDuckPlayer) + } +} diff --git a/UnitTests/YoutubePlayer/DuckPlayerOnboardingLocationValidatorTests.swift b/UnitTests/YoutubePlayer/DuckPlayerOnboardingLocationValidatorTests.swift new file mode 100644 index 0000000000..09b2329471 --- /dev/null +++ b/UnitTests/YoutubePlayer/DuckPlayerOnboardingLocationValidatorTests.swift @@ -0,0 +1,93 @@ +// +// DuckPlayerOnboardingLocationValidatorTests.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 XCTest +@testable import DuckDuckGo_Privacy_Browser +import Navigation +class DuckPlayerOnboardingLocationValidatorTests: XCTestCase { + + var validator: DuckPlayerOnboardingLocationValidator! + + override func setUp() { + super.setUp() + validator = DuckPlayerOnboardingLocationValidator() + } + + @MainActor + func testIsValidLocation_RootURL_ReturnsTrue() async { + let webView = MockWebView() + webView.mockURL = URL(string: "https://www.youtube.com/") + let result = await validator.isValidLocation(webView) + XCTAssertTrue(result) + } + + @MainActor + func testIsValidLocation_NonRootURL_ReturnsFalse() async { + let webView = MockWebView() + webView.mockURL = URL(string: "https://www.youtube.com/duckduckgo") + let result = await validator.isValidLocation(webView) + XCTAssertFalse(result) + } + + @MainActor + func testIsValidLocation_NonYoutubeURL_ReturnsFalse() async { + let webView = MockWebView() + webView.mockURL = URL(string: "https://www.duckduckgo.com/") + let result = await validator.isValidLocation(webView) + XCTAssertFalse(result) + } + + @MainActor + func testIsValidLocation_NoURL_ReturnsFalse() async { + let webView = MockWebView() + webView.mockURL = nil + let result = await validator.isValidLocation(webView) + XCTAssertFalse(result) + } + + @MainActor + func testIsCurrentWebViewInAYoutubeChannel_ChannelURL_ReturnsTrue() async { + let webView = MockWebView() + webView.mockURL = URL(string: "https://www.youtube.com/duckduckgo")! + webView.evaluateJavaScriptResult = true + let result = await validator.isValidLocation(webView) + XCTAssertTrue(result) + } + + @MainActor + func testIsCurrentWebViewInAYoutubeChannel_NonChannelURL_ReturnsFalse() async { + let webView = MockWebView() + webView.mockURL = URL(string: "https://www.youtube.com/settings")! + webView.evaluateJavaScriptResult = false + let result = await validator.isValidLocation(webView) + XCTAssertFalse(result) + } + +} +private final class MockWebView: WKWebView { + var mockURL: URL? + var evaluateJavaScriptResult: Any? + + override var url: URL? { + return mockURL + } + + override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) { + completionHandler?(evaluateJavaScriptResult, nil) + } +} From 3999929fb1ce8bb155cb3b85b30da0eebbe69d25 Mon Sep 17 00:00:00 2001 From: Lucas Adamski Date: Fri, 30 Aug 2024 15:53:30 -0700 Subject: [PATCH 02/16] remove references to obsolete FBConfig (#3180) Task/Issue URL: https://app.asana.com/0/1171671347221384/1208194339440141/f Tech Design URL: CC: **Description**: Remove all references to the obsolete FBConfig file, which has been replaced by the shared privacy config **Steps to test this PR**: 1. **Definition of Done**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/Application/AppConfigurationURLProvider.swift | 1 - DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift | 1 - DuckDuckGo/Configuration/ConfigurationStore.swift | 7 ------- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../AppDelegate/AppConfigurationURLProviderTests.swift | 1 - 9 files changed, 6 insertions(+), 16 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7742eb8eaa..1942ade226 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13740,7 +13740,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 190.0.0; + version = 190.0.1; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6c396add94..d2753d8257 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "ac53011582abcca4aefd66f15308332273eecb49", - "version" : "190.0.0" + "revision" : "245750c9ca559813307641e819fb27c6d294339f", + "version" : "190.0.1" } }, { diff --git a/DuckDuckGo/Application/AppConfigurationURLProvider.swift b/DuckDuckGo/Application/AppConfigurationURLProvider.swift index a0aa950a9b..188a95d698 100644 --- a/DuckDuckGo/Application/AppConfigurationURLProvider.swift +++ b/DuckDuckGo/Application/AppConfigurationURLProvider.swift @@ -58,7 +58,6 @@ struct AppConfigurationURLProvider: ConfigurationURLProviding { case .surrogates: return URL(string: "https://staticcdn.duckduckgo.com/surrogates.txt")! case .trackerDataSet: return URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/v6/current/macos-tds.json")! // In archived repo, to be refactored shortly (https://staticcdn.duckduckgo.com/useragents/social_ctp_configuration.json) - case .FBConfig: return URL(string: "https://staticcdn.duckduckgo.com/useragents/")! case .remoteMessagingConfig: return RemoteMessagingClient.Constants.endpoint } } diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 20214ac6cc..bc5dffb816 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -51,7 +51,6 @@ public struct UserDefaultsWrapper { case configStorageBloomFilterExclusionsEtag = "config.storage.bloomfilter.exclusions.etag" case configStorageSurrogatesEtag = "config.storage.surrogates.etag" case configStoragePrivacyConfigurationEtag = "config.storage.privacyconfiguration.etag" - case configFBConfigEtag = "config.storage.fbconfig.etag" case configStorageRemoteMessagingConfigEtag = "config.storage.remotemessagingconfig.etag" case configLastInstalled = "config.last.installed" diff --git a/DuckDuckGo/Configuration/ConfigurationStore.swift b/DuckDuckGo/Configuration/ConfigurationStore.swift index 5d21979c29..0f9b5ddcfd 100644 --- a/DuckDuckGo/Configuration/ConfigurationStore.swift +++ b/DuckDuckGo/Configuration/ConfigurationStore.swift @@ -31,7 +31,6 @@ final class ConfigurationStore: ConfigurationStoring { .surrogates: "surrogates.txt", .privacyConfiguration: "macos-config.json", .trackerDataSet: "tracker-radar.json", - .FBConfig: "social_ctp_configuration.json", .remoteMessagingConfig: "remote-messaging-config.json" ] @@ -55,9 +54,6 @@ final class ConfigurationStore: ConfigurationStoring { @UserDefaultsWrapper(key: .configStoragePrivacyConfigurationEtag, defaultValue: nil) private var privacyConfigurationEtag: String? - @UserDefaultsWrapper(key: .configFBConfigEtag, defaultValue: nil) - private var FBConfigEtag: String? - @UserDefaultsWrapper(key: .configStorageRemoteMessagingConfigEtag, defaultValue: nil) private var remoteMessagingConfigEtag: String? @@ -71,7 +67,6 @@ final class ConfigurationStore: ConfigurationStoring { case .surrogates: return surrogatesEtag case .trackerDataSet: return trackerRadarEtag case .privacyConfiguration: return privacyConfigurationEtag - case .FBConfig: return FBConfigEtag case .remoteMessagingConfig: return remoteMessagingConfigEtag } } @@ -92,7 +87,6 @@ final class ConfigurationStore: ConfigurationStoring { case .surrogates: surrogatesEtag = etag case .trackerDataSet: trackerRadarEtag = etag case .privacyConfiguration: privacyConfigurationEtag = etag - case .FBConfig: FBConfigEtag = etag case .remoteMessagingConfig: remoteMessagingConfigEtag = etag } } @@ -126,7 +120,6 @@ final class ConfigurationStore: ConfigurationStoring { Logger.config.info("surrogatesEtag \(self.surrogatesEtag ?? "", privacy: .public)") Logger.config.info("trackerRadarEtag \(self.trackerRadarEtag ?? "", privacy: .public)") Logger.config.info("privacyConfigurationEtag \(self.privacyConfigurationEtag ?? "", privacy: .public)") - Logger.config.info("FBConfigEtag \(self.FBConfigEtag ?? "", privacy: .public)") Logger.config.info("remoteMessagingConfig \(self.remoteMessagingConfigEtag ?? "", privacy: .public)") } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index a79685ddb4..068b038904 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: "190.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "190.0.1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index b1da627366..804b34fd0f 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: "190.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "190.0.1"), .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 dc054cbeb6..002fad05cc 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: "190.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "190.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/AppDelegate/AppConfigurationURLProviderTests.swift b/UnitTests/AppDelegate/AppConfigurationURLProviderTests.swift index b23486adf8..2fc10c8ce0 100644 --- a/UnitTests/AppDelegate/AppConfigurationURLProviderTests.swift +++ b/UnitTests/AppDelegate/AppConfigurationURLProviderTests.swift @@ -28,7 +28,6 @@ final class AppConfigurationURLProviderTests: XCTestCase { XCTAssertEqual(AppConfigurationURLProvider().url(for: .privacyConfiguration).absoluteString, "https://staticcdn.duckduckgo.com/trackerblocking/config/v4/macos-config.json") XCTAssertEqual(AppConfigurationURLProvider().url(for: .surrogates).absoluteString, "https://staticcdn.duckduckgo.com/surrogates.txt") XCTAssertEqual(AppConfigurationURLProvider().url(for: .trackerDataSet).absoluteString, "https://staticcdn.duckduckgo.com/trackerblocking/v6/current/macos-tds.json") - XCTAssertEqual(AppConfigurationURLProvider().url(for: .FBConfig).absoluteString, "https://staticcdn.duckduckgo.com/useragents/") } } From dd2ef9d261330028641c7ba16475202267aaa585 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Sun, 1 Sep 2024 17:38:56 +0200 Subject: [PATCH 03/16] BSK update for new Sync Promotion feature flag (#3128) Task/Issue URL: https://app.asana.com/0/72649045549333/1208017419957473/f Tech Design URL: CC: **Description**: Updates for new Sync Promotion feature flag in BSK --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1942ade226..23a26dce40 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13740,7 +13740,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 190.0.1; + version = 190.0.2; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d2753d8257..c8fd98dd61 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "245750c9ca559813307641e819fb27c6d294339f", - "version" : "190.0.1" + "revision" : "2e9282d79f4a36ad851e9e130ffd936a5c8e74c7", + "version" : "190.0.2" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 068b038904..f3d5b786fc 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: "190.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "190.0.2"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 804b34fd0f..ed5de52d35 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: "190.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "190.0.2"), .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 002fad05cc..1be5db71c4 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: "190.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "190.0.2"), .package(path: "../SwiftUIExtensions") ], targets: [ From a0bad2499e23aa22cabb9641dcc6f0c8963aa07b Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Mon, 2 Sep 2024 12:13:57 +1000 Subject: [PATCH 04/16] Fix site owned by major network (#3168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1176956903599313/1208164650000833/f **Description**: There’s an existing [issue](https://app.asana.com/0/0/1207970856546695/1208097565947206/f) that causes showing the wrong education dialog for websites that are owned by a major network like Facebook and Google. In addition, the Privacy Dashboard [does not reflect weather an entity is owned by a major tracker](https://app.asana.com/0/0/1207970856546695/1208114308033513/f) This PR exposes the field “ownedBy” with the purpose of identifying when a tracker is owned by another entity. e.g. Instagram is ownedBy is Facebook --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- .../Tab/TabExtensions/PrivacyDashboardTabExtension.swift | 2 +- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../ContentBlocker/ContentBlockingUpdatingTests.swift | 3 ++- 7 files changed, 11 insertions(+), 10 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 23a26dce40..596675d527 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13740,7 +13740,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 190.0.2; + version = 191.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c8fd98dd61..dd90976540 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "2e9282d79f4a36ad851e9e130ffd936a5c8e74c7", - "version" : "190.0.2" + "revision" : "bbfd3f12ffbc958a7f97f2ccb4779025a44380b5", + "version" : "191.0.0" } }, { @@ -176,8 +176,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { - "revision" : "1403e17eeeb8493b92fb9d11eb8c846bb9776581", - "version" : "2.1.2" + "revision" : "5de0a610a7927b638a5fd463a53032c9934a2c3b", + "version" : "3.0.0" } }, { diff --git a/DuckDuckGo/Tab/TabExtensions/PrivacyDashboardTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/PrivacyDashboardTabExtension.swift index 7d2900d390..486b20ead8 100644 --- a/DuckDuckGo/Tab/TabExtensions/PrivacyDashboardTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/PrivacyDashboardTabExtension.swift @@ -114,7 +114,7 @@ extension PrivacyDashboardTabExtension { private func makePrivacyInfo(url: URL) -> PrivacyInfo? { guard let host = url.host else { return nil } - let entity = contentBlocking.trackerDataManager.trackerData.findEntity(forHost: host) + let entity = contentBlocking.trackerDataManager.trackerData.findParentEntityOrFallback(forHost: host) privacyInfo = PrivacyInfo(url: url, parentEntity: entity, diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index f3d5b786fc..4765dd98bb 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: "190.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "191.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index ed5de52d35..1787726897 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: "190.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "191.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 1be5db71c4..11db3dd457 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: "190.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "191.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift b/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift index da76ce79d6..524c1501a5 100644 --- a/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift +++ b/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift @@ -144,7 +144,8 @@ final class ContentBlockingUpdatingTests: XCTestCase { static let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, owner: KnownTracker.Owner(name: "Tracker Inc", - displayName: "Tracker Inc company"), + displayName: "Tracker Inc company", + ownedBy: "Owner"), prevalence: 0.1, subdomains: nil, categories: nil, From 6197607e7834ad9cd6c259547b7eecce064793f4 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 2 Sep 2024 10:39:34 +0200 Subject: [PATCH 05/16] Migrate asana-get-automation-subtask-id from GHA action to fastlane (#3175) Task/Issue URL: https://app.asana.com/0/0/1208137627434493/f Description: This change removes asana-get-automation-subtask-id action, replacing it with a fastlane action from the automation plugin. --- .../asana-create-action-item/action.yml | 18 ++++----- .../action.yml | 39 ------------------- .github/actions/asana-log-message/action.yml | 14 +++---- Gemfile.lock | 6 +-- fastlane/Pluginfile | 2 +- 5 files changed, 20 insertions(+), 59 deletions(-) delete mode 100644 .github/actions/asana-get-release-automation-subtask-id/action.yml diff --git a/.github/actions/asana-create-action-item/action.yml b/.github/actions/asana-create-action-item/action.yml index b83f2dd1ad..740766bc5b 100644 --- a/.github/actions/asana-create-action-item/action.yml +++ b/.github/actions/asana-create-action-item/action.yml @@ -38,10 +38,10 @@ runs: using: "composite" steps: - id: get-automation-subtask - uses: ./.github/actions/asana-get-release-automation-subtask-id - with: - access-token: ${{ inputs.access-token }} - task-url: ${{ inputs.release-task-url }} + shell: bash + env: + ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} + run: bundle exec fastlane run asana_get_release_automation_subtask_id task_url:"${{ inputs.release-task-url }}" - id: get-asana-user-id if: github.event_name != 'schedule' @@ -53,7 +53,7 @@ runs: - id: set-assignee-id shell: bash env: - USER_ID: ${{ steps.get-asana-user-id.outputs.user-id || steps.get-automation-subtask.outputs.assignee-id }} + USER_ID: ${{ steps.get-asana-user-id.outputs.user-id || steps.get-automation-subtask.outputs.asana_assignee_id }} run: | echo "assignee-id=${USER_ID}" >> $GITHUB_OUTPUT @@ -62,7 +62,7 @@ runs: shell: bash env: ASSIGNEE_ID: ${{ steps.set-assignee-id.outputs.assignee-id }} - AUTOMATION_TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + AUTOMATION_TASK_ID: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} TEMPLATE_PATH: ${{ github.action_path }}/templates/${{ inputs.template-name }}.yml WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | @@ -80,7 +80,7 @@ runs: shell: bash env: ASSIGNEE_ID: ${{ steps.set-assignee-id.outputs.assignee-id }} - AUTOMATION_TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + AUTOMATION_TASK_ID: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} NOTES: ${{ inputs.notes }} TASK_NAME: ${{ inputs.task-name }} WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -93,7 +93,7 @@ runs: shell: bash env: ASSIGNEE_ID: ${{ steps.set-assignee-id.outputs.assignee-id }} - AUTOMATION_TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + AUTOMATION_TASK_ID: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} HTML_NOTES: ${{ inputs.html-notes }} TASK_NAME: ${{ inputs.task-name }} WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -106,7 +106,7 @@ runs: env: ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} ASSIGNEE_ID: ${{ steps.set-assignee-id.outputs.assignee-id }} - TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + TASK_ID: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} PAYLOAD_BASE64: ${{ steps.process-template-payload.outputs.payload-base64 || steps.process-notes-payload.outputs.payload-base64 || steps.process-html-notes-payload.outputs.payload-base64 }} run: | # Create a subtask and retrieve its ID from the response (.data.gid) to return as an output diff --git a/.github/actions/asana-get-release-automation-subtask-id/action.yml b/.github/actions/asana-get-release-automation-subtask-id/action.yml deleted file mode 100644 index 0b50aa37bc..0000000000 --- a/.github/actions/asana-get-release-automation-subtask-id/action.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Get Release Automation subtask ID -description: Finds 'Automation' subtask in the release task and returns its ID -inputs: - access-token: - description: "Asana access token" - required: true - type: string - task-url: - description: "Asana release task URL" - required: true - type: string -outputs: - automation-task-id: - description: "Automation task ID" - value: ${{ steps.extract-automation-task-id.outputs.automation-task-id }} - assignee-id: - description: "Release task assignee ID" - value: ${{ steps.extract-assignee-id.outputs.asana_assignee_id }} -runs: - using: "composite" - steps: - - id: extract-task-id - shell: bash - run: bundle exec fastlane run asana_extract_task_id task_url:"${{ inputs.task-url }}" - - - id: extract-assignee-id - shell: bash - env: - ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} - run: bundle exec fastlane run asana_extract_task_assignee task_id:"${{ steps.extract-task-id.outputs.asana_task_id }}" - - - id: extract-automation-task-id - run: | - task_id="${{ steps.extract-task-id.outputs.asana_task_id }}" - automation_task_id=$(curl -fLSs "https://app.asana.com/api/1.0/tasks/${task_id}/subtasks?opt_fields=name" \ - -H "Authorization: Bearer ${{ inputs.access-token }}" \ - | jq -r '.data[] | select(.name=="Automation").gid') - echo "automation-task-id=${automation_task_id}" >> $GITHUB_OUTPUT - shell: bash diff --git a/.github/actions/asana-log-message/action.yml b/.github/actions/asana-log-message/action.yml index e00cbd2a6d..cffd0bec66 100644 --- a/.github/actions/asana-log-message/action.yml +++ b/.github/actions/asana-log-message/action.yml @@ -24,10 +24,10 @@ runs: using: "composite" steps: - id: get-automation-subtask - uses: ./.github/actions/asana-get-release-automation-subtask-id - with: - access-token: ${{ inputs.access-token }} - task-url: ${{ inputs.task-url }} + shell: bash + env: + ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} + run: bundle exec fastlane run asana_get_release_automation_subtask_id task_url:"${{ inputs.task-url }}" - id: get-asana-user-id if: github.event_name != 'schedule' @@ -40,8 +40,8 @@ runs: shell: bash env: ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} - ASSIGNEE_ID: ${{ steps.get-asana-user-id.outputs.user-id || steps.get-automation-subtask.outputs.assignee-id }} - TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + ASSIGNEE_ID: ${{ steps.get-asana-user-id.outputs.user-id || steps.get-automation-subtask.outputs.asana_assignee_id }} + TASK_ID: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} run: | return_code=$(curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}/addFollowers" \ -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ @@ -59,6 +59,6 @@ runs: uses: ./.github/actions/asana-add-comment with: access-token: ${{ inputs.access-token }} - task-id: ${{ steps.get-automation-subtask.outputs.automation-task-id }} + task-id: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} comment: ${{ inputs.comment }} template-name: ${{ inputs.template-name }} diff --git a/Gemfile.lock b/Gemfile.lock index 1277000bcc..fd3523c45a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation - revision: 5d267b66e9c63f879eecbb4ae1f40cb42547026e - tag: 0.2.0 + revision: 936568643e0d295885af809aa9857448a4141a81 + tag: 0.3.0 specs: - fastlane-plugin-ddg_apple_automation (0.2.0) + fastlane-plugin-ddg_apple_automation (0.3.0) GEM remote: https://rubygems.org/ diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index cd249a9c24..18ff0d994a 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -2,4 +2,4 @@ # # Ensure this file is checked in to source control! -gem 'fastlane-plugin-ddg_apple_automation', git: 'https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation', tag: '0.2.0' +gem 'fastlane-plugin-ddg_apple_automation', git: 'https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation', tag: '0.3.0' From cd79a76db1f744ec39a52163859038714ea97e11 Mon Sep 17 00:00:00 2001 From: amddg44 Date: Mon, 2 Sep 2024 11:27:37 +0200 Subject: [PATCH 06/16] Adds support for new Autofill unknownUsernameCategorization feature flag (#3170) Task/Issue URL: https://app.asana.com/0/1201462886803403/1208150709359292/f Tech Design URL: CC: Description: Adds support for new autofill feature flag unknownUsernameCategorization and includes it in the ContentScopeFeatureToggles for consumption by autofill.js --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/DBP/DBPHomeViewController.swift | 3 ++- DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift | 5 +++++ DuckDuckGo/Tab/Model/ContentScopeFeatureFlagging.swift | 3 ++- LocalPackages/DataBrokerProtection/Package.swift | 2 +- .../DebugUI/DataBrokerRunCustomJSONViewModel.swift | 3 ++- .../Scheduler/DataBrokerProtectionAgentManager.swift | 3 ++- .../Tests/DataBrokerProtectionTests/Mocks.swift | 3 ++- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 11 files changed, 21 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 596675d527..d58b3a866f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13740,7 +13740,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 191.0.0; + version = 191.0.1; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index dd90976540..51adade877 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "bbfd3f12ffbc958a7f97f2ccb4779025a44380b5", - "version" : "191.0.0" + "revision" : "086d8e74c85783bf27df03f5ac2586a910d69155", + "version" : "191.0.1" } }, { diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index 6da4daef5e..9638418afd 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -50,7 +50,8 @@ final class DBPHomeViewController: NSViewController { credentialsSaving: false, passwordGeneration: false, inlineIconCredentials: false, - thirdPartyCredentialsProvider: false) + thirdPartyCredentialsProvider: false, + unknownUsernameCategorization: false) let isGPCEnabled = WebTrackingProtectionPreferences.shared.isGPCEnabled let sessionKey = UUID().uuidString diff --git a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift index 11919d7bd0..fd9e360536 100644 --- a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift @@ -32,6 +32,9 @@ public enum FeatureFlag: String { // https://app.asana.com/0/1206488453854252/1207136666798700/f case freemiumPIR + + // https://app.asana.com/0/1201462886803403/1208030658792310/f + case unknownUsernameCategorization } extension FeatureFlag: FeatureFlagSourceProviding { @@ -45,6 +48,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .remoteReleasable(.subfeature(sslCertificatesSubfeature.allowBypass)) case .deduplicateLoginsOnImport: return .remoteReleasable(.subfeature(AutofillSubfeature.deduplicateLoginsOnImport)) + case .unknownUsernameCategorization: + return .remoteReleasable(.subfeature(AutofillSubfeature.unknownUsernameCategorization)) case .freemiumPIR: return .remoteDevelopment(.subfeature(DBPSubfeature.freemium)) } diff --git a/DuckDuckGo/Tab/Model/ContentScopeFeatureFlagging.swift b/DuckDuckGo/Tab/Model/ContentScopeFeatureFlagging.swift index 41272ed324..09010c106e 100644 --- a/DuckDuckGo/Tab/Model/ContentScopeFeatureFlagging.swift +++ b/DuckDuckGo/Tab/Model/ContentScopeFeatureFlagging.swift @@ -31,6 +31,7 @@ extension ContentScopeFeatureToggles { credentialsSaving: autofillPrefs.askToSaveUsernamesAndPasswords, passwordGeneration: autofillPrefs.askToSaveUsernamesAndPasswords, inlineIconCredentials: autofillPrefs.askToSaveUsernamesAndPasswords, - thirdPartyCredentialsProvider: true) + thirdPartyCredentialsProvider: true, + unknownUsernameCategorization: privacyConfig.isSubfeatureEnabled(AutofillSubfeature.unknownUsernameCategorization)) } } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 4765dd98bb..dc3ba2d552 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: "191.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "191.0.1"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index 1a9064acee..11f3086983 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -164,7 +164,8 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { credentialsSaving: false, passwordGeneration: false, inlineIconCredentials: false, - thirdPartyCredentialsProvider: false) + thirdPartyCredentialsProvider: false, + unknownUsernameCategorization: false) let sessionKey = UUID().uuidString self.authenticationManager = authenticationManager diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift index facbfb9dad..7c7630ca93 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift @@ -43,7 +43,8 @@ public class DataBrokerProtectionAgentManagerProvider { credentialsSaving: false, passwordGeneration: false, inlineIconCredentials: false, - thirdPartyCredentialsProvider: false) + thirdPartyCredentialsProvider: false, + unknownUsernameCategorization: false) let contentScopeProperties = ContentScopeProperties(gpcEnabled: false, sessionKey: UUID().uuidString, featureToggles: features) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 27874b9905..fa4e8ce759 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -252,7 +252,8 @@ extension ContentScopeFeatureToggles { credentialsSaving: false, passwordGeneration: false, inlineIconCredentials: false, - thirdPartyCredentialsProvider: false + thirdPartyCredentialsProvider: false, + unknownUsernameCategorization: false ) } } diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 1787726897..71ae77642e 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: "191.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "191.0.1"), .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 11db3dd457..d35e2fea45 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: "191.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "191.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ From 1d4f93f2209af85c4970b387bc71a1326154fe78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Mon, 2 Sep 2024 12:51:14 +0200 Subject: [PATCH 07/16] Migrate asana-upload from GHA action to fastlane (#3179) Task/Issue URL: https://app.asana.com/0/1201392122292466/1208137627434497/f This change adds new lane that replaces asana-upload action --- .github/workflows/build_notarized.yml | 11 ++++---- .github/workflows/publish_dmg_release.yml | 33 ++++++++++++----------- Gemfile.lock | 8 +++--- fastlane/Pluginfile | 2 +- 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build_notarized.yml b/.github/workflows/build_notarized.yml index 1db6405485..fc24455ba4 100644 --- a/.github/workflows/build_notarized.yml +++ b/.github/workflows/build_notarized.yml @@ -352,11 +352,12 @@ jobs: - name: Upload DMG to Asana if: ${{ env.upload-to == 'asana' }} - uses: ./.github/actions/asana-upload - with: - access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} - file-name: ${{ github.workspace }}/duckduckgo-${{ env.app-version }}.dmg - task-id: ${{ steps.task-id.outputs.asana_task_id }} + env: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + run: | + bundle exec fastlane run asana_upload \ + file_name:"${{ github.workspace }}/duckduckgo-${{ env.app-version }}.dmg" \ + task_id:"${{ steps.task-id.outputs.asana_task_id }}" mattermost: diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml index fa476c8ee5..5fdc98c79e 100644 --- a/.github/workflows/publish_dmg_release.yml +++ b/.github/workflows/publish_dmg_release.yml @@ -345,29 +345,32 @@ jobs: - name: Upload patch to the Asana task id: upload-patch if: success() - uses: ./.github/actions/asana-upload - with: - access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} - file-name: ${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }} - task-id: ${{ steps.create-task.outputs.new-task-id }} + env: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + run: | + bundle exec fastlane run asana_upload \ + file_name:"${{ env.SPARKLE_DIR }}/${{ steps.appcast.outputs.appcast-patch-name }}" \ + task_id:"${{ steps.create-task.outputs.new-task-id }}" - name: Upload old appcast file to the Asana task id: upload-old-appcast if: success() - uses: ./.github/actions/asana-upload - with: - access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} - file-name: ${{ env.OLD_APPCAST_NAME }} - task-id: ${{ steps.create-task.outputs.new-task-id }} + env: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + run: | + bundle exec fastlane run asana_upload \ + file_name:"${{ env.OLD_APPCAST_NAME }}" \ + task_id:"${{ steps.create-task.outputs.new-task-id }}" - name: Upload release notes to the Asana task id: upload-release-notes if: success() - uses: ./.github/actions/asana-upload - with: - access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} - file-name: ${{ env.RELEASE_NOTES_FILE }} - task-id: ${{ steps.create-task.outputs.new-task-id }} + env: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + run: | + bundle exec fastlane run asana_upload \ + file_name:"${{ env.RELEASE_NOTES_FILE }}" \ + task_id:"${{ steps.create-task.outputs.new-task-id }}" - name: Report status if: always() diff --git a/Gemfile.lock b/Gemfile.lock index fd3523c45a..c5e8d3215e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation - revision: 936568643e0d295885af809aa9857448a4141a81 - tag: 0.3.0 + revision: a207fdee88bd26b877c973dfbf45038eff19ec17 + tag: 0.4.0 specs: - fastlane-plugin-ddg_apple_automation (0.3.0) + fastlane-plugin-ddg_apple_automation (0.4.0) GEM remote: https://rubygems.org/ @@ -231,4 +231,4 @@ DEPENDENCIES httparty BUNDLED WITH - 2.4.19 + 2.4.4 diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index 18ff0d994a..b5f9d5d568 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -2,4 +2,4 @@ # # Ensure this file is checked in to source control! -gem 'fastlane-plugin-ddg_apple_automation', git: 'https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation', tag: '0.3.0' +gem 'fastlane-plugin-ddg_apple_automation', git: 'https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation', tag: '0.4.0' From be362cb3f0e82b30b6d8f85dab8ee1df9573a1fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Mon, 2 Sep 2024 14:32:47 +0200 Subject: [PATCH 08/16] Migrate asana get user id for github handle action (#3184) Task/Issue URL: https://app.asana.com/0/1203790657351911/1208137627436558/f Migrate asana-get-user-id-for-github-handle action to fastlane --- .github/actions/asana-create-action-item/action.yml | 8 +++----- .github/actions/asana-log-message/action.yml | 8 +++----- .github/workflows/code_freeze.yml | 8 +++----- Gemfile.lock | 10 +++++----- fastlane/Pluginfile | 2 +- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/.github/actions/asana-create-action-item/action.yml b/.github/actions/asana-create-action-item/action.yml index 740766bc5b..a5adf09d9d 100644 --- a/.github/actions/asana-create-action-item/action.yml +++ b/.github/actions/asana-create-action-item/action.yml @@ -44,16 +44,14 @@ runs: run: bundle exec fastlane run asana_get_release_automation_subtask_id task_url:"${{ inputs.release-task-url }}" - id: get-asana-user-id + shell: bash if: github.event_name != 'schedule' - uses: duckduckgo/apple-toolbox/actions/asana-get-user-id-for-github-handle@main - with: - access-token: ${{ inputs.access-token }} - github-handle: ${{ github.actor }} + run: bundle exec fastlane run asana_get_user_id_for_github_handle github_handle:"${{ github.actor }}" - id: set-assignee-id shell: bash env: - USER_ID: ${{ steps.get-asana-user-id.outputs.user-id || steps.get-automation-subtask.outputs.asana_assignee_id }} + USER_ID: ${{ steps.get-asana-user-id.outputs.asana_user_id || steps.get-automation-subtask.outputs.asana_assignee_id }} run: | echo "assignee-id=${USER_ID}" >> $GITHUB_OUTPUT diff --git a/.github/actions/asana-log-message/action.yml b/.github/actions/asana-log-message/action.yml index cffd0bec66..fac2c2b2d3 100644 --- a/.github/actions/asana-log-message/action.yml +++ b/.github/actions/asana-log-message/action.yml @@ -30,17 +30,15 @@ runs: run: bundle exec fastlane run asana_get_release_automation_subtask_id task_url:"${{ inputs.task-url }}" - id: get-asana-user-id + shell: bash if: github.event_name != 'schedule' - uses: duckduckgo/apple-toolbox/actions/asana-get-user-id-for-github-handle@main - with: - access-token: ${{ inputs.access-token }} - github-handle: ${{ github.actor }} + run: bundle exec fastlane run asana_get_user_id_for_github_handle github_handle:"${{ github.actor }}" - id: add-colaborator shell: bash env: ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} - ASSIGNEE_ID: ${{ steps.get-asana-user-id.outputs.user-id || steps.get-automation-subtask.outputs.asana_assignee_id }} + ASSIGNEE_ID: ${{ steps.get-asana-user-id.outputs.asana_user_id || steps.get-automation-subtask.outputs.asana_assignee_id }} TASK_ID: ${{ steps.get-automation-subtask.outputs.asana_automation_task_id }} run: | return_code=$(curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}/addFollowers" \ diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml index f80bd09e71..9d66ce6cc0 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -47,16 +47,14 @@ jobs: - name: Get Asana user ID id: get-asana-user-id - uses: duckduckgo/apple-toolbox/actions/asana-get-user-id-for-github-handle@main - with: - access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} - github-handle: ${{ github.actor }} + shell: bash + run: bundle exec fastlane run asana_get_user_id_for_github_handle github_handle:"${{ github.actor }}" - name: Create release task id: create_release_task env: ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} - ASSIGNEE_ID: ${{ steps.get-asana-user-id.outputs.user-id }} + ASSIGNEE_ID: ${{ steps.get-asana-user-id.outputs.asana_user_id }} run: | version="$(echo ${{ steps.make_release_branch.outputs.release_branch_name }} | cut -d '/' -f 2)" task_name="macOS App Release $version" diff --git a/Gemfile.lock b/Gemfile.lock index c5e8d3215e..9be15d591e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation - revision: a207fdee88bd26b877c973dfbf45038eff19ec17 - tag: 0.4.0 + revision: 857c55c75a1153746a2db89710b6d69c241d534f + tag: 0.5.0 specs: - fastlane-plugin-ddg_apple_automation (0.4.0) + fastlane-plugin-ddg_apple_automation (0.5.0) GEM remote: https://rubygems.org/ @@ -17,8 +17,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.968.0) - aws-sdk-core (3.201.5) + aws-partitions (1.970.0) + aws-sdk-core (3.202.2) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.9) diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index b5f9d5d568..745ff05f20 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -2,4 +2,4 @@ # # Ensure this file is checked in to source control! -gem 'fastlane-plugin-ddg_apple_automation', git: 'https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation', tag: '0.4.0' +gem 'fastlane-plugin-ddg_apple_automation', git: 'https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation', tag: '0.5.0' From ea7750ff8e7b83ac44b2fd99687990b4fdfeea26 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 2 Sep 2024 12:37:34 +0000 Subject: [PATCH 09/16] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +- DuckDuckGo/ContentBlocker/macos-config.json | 189 +++++------------- 2 files changed, 47 insertions(+), 146 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index 9e52626554..7988b7b423 100644 --- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"e2e01f769d64b248bff516166ddb19f5\"" - public static let embeddedDataSHA = "aea5eeea59fba229fdf46a41b82ebc917593a5f2b86478a1c504c24df3632195" + public static let embeddedDataETag = "\"c44d102eca183ab66c46945a42d2d08c\"" + public static let embeddedDataSHA = "6a608de7ef93edbd8c700aac9a879cf6dd9f4cef9868c06433fe8fe28106e9af" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index b013b6528e..72b0aa504d 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1724668148700, + "version": 1725043256685, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -86,9 +86,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -117,7 +114,7 @@ ] }, "state": "enabled", - "hash": "f480c22abb392f288fbf2caf186ce082" + "hash": "fa5f86bac5946c528cd6bc7449a2718a" }, "androidBrowserConfig": { "exceptions": [], @@ -293,9 +290,6 @@ { "domain": "bitsofwar.com" }, - { - "domain": "mitglieder.franzspitzer.de" - }, { "domain": "disneyplus.com" }, @@ -323,12 +317,6 @@ { "domain": "hertz.com" }, - { - "domain": "www.thrifty.com" - }, - { - "domain": "sports.tipico.de" - }, { "domain": "lotusbakeries.com" }, @@ -356,9 +344,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -374,10 +359,10 @@ "state": "enabled", "features": { "filterlistExperiment": { - "state": "disabled" + "state": "enabled" } }, - "hash": "47b8af08b78f08d4ac808469f1a2bd98" + "hash": "0f117021faa53765286cc70d041a5bc2" }, "autofillBreakageReporter": { "state": "disabled", @@ -397,9 +382,12 @@ "features": { "deduplicateLoginsOnImport": { "state": "enabled" + }, + "unknownUsernameCategorization": { + "state": "enabled" } }, - "hash": "1832dd4088cba6eaf3a9776a6fc9a1f5" + "hash": "4fb2ab2ceed78c92aac780bc7ae8d4a2" }, "breakageReporting": { "state": "disabled", @@ -416,14 +404,11 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], - "hash": "1cc5be3534e326e74083566308e726f4" + "hash": "37e0cf88badfc8b01b6394f0884502f6" }, "brokenSitePrompt": { "state": "disabled", @@ -1120,9 +1105,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "tinder.com" }, @@ -1146,7 +1128,7 @@ }, "state": "enabled", "minSupportedVersion": "1.93.0", - "hash": "ca0e8ea2cfbd1c60b0cbd08bc9545ff1" + "hash": "81933e08c573f41c7a7e1620de3e2dbd" }, "clickToPlay": { "exceptions": [ @@ -1162,9 +1144,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -1179,7 +1158,7 @@ } }, "state": "enabled", - "hash": "aa1e279f495037feb71c38538ab25479" + "hash": "f48fcbcedd692bef6e3279e323727b54" }, "clientBrandHint": { "exceptions": [], @@ -1222,14 +1201,11 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], - "hash": "51628b06bd28538540ce9e5262e42070" + "hash": "96b2f778bab196aa424e9c859ddea778" }, "cookie": { "settings": { @@ -1292,15 +1268,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "518de3f4cdd6f78e664c00ea2ec349cb" + "hash": "6b4d2cef180104c5c84f5687479b8492" }, "customUserAgent": { "settings": { @@ -1522,9 +1495,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "wunderground.com" }, @@ -4797,7 +4767,7 @@ ] }, "state": "enabled", - "hash": "10a59471aab6744f176b20577b7caacb" + "hash": "85f1dc0d2a2bcd309d8683c058b7aa52" }, "exceptionHandler": { "exceptions": [ @@ -4813,15 +4783,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "4818e6983b16ddfdd078a2565bc69859" + "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" }, "extendedOnboarding": { "exceptions": [], @@ -4846,14 +4813,11 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], - "hash": "429a0ad38c859268f48d96c4c46864c6" + "hash": "7f042650922da2636492e77ed1101bce" }, "fingerprintingBattery": { "exceptions": [ @@ -4872,15 +4836,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "f3d4db6f69a4da53cfb1de3ac601b3a1" + "hash": "b9993a70bb5d6bfba23b4ec797f8a684" }, "fingerprintingCanvas": { "settings": { @@ -4983,15 +4944,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "69e97b215250895325a279ae6203e6fe" + "hash": "49a3d497835bf5715aaaa73f87dd974f" }, "fingerprintingHardware": { "settings": { @@ -5061,9 +5019,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "airbnb.com" }, @@ -5102,7 +5057,7 @@ } ], "state": "enabled", - "hash": "e387aaf98e1ce90af9678f5a6c80020d" + "hash": "97e86f6982f80cebfd56915be1457595" }, "fingerprintingScreenSize": { "settings": { @@ -5154,15 +5109,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "b6db688f3bd599c6906efa98f70254ae" + "hash": "9c9c85ea83b8e7c6d07c20d1539a000f" }, "fingerprintingTemporaryStorage": { "exceptions": [ @@ -5187,15 +5139,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "92bfb7484b6c52115159ef47d5c82b44" + "hash": "2dc4c0ade2e385d208695d770002c725" }, "googleRejected": { "exceptions": [ @@ -5211,15 +5160,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "4818e6983b16ddfdd078a2565bc69859" + "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" }, "gpc": { "state": "enabled", @@ -5284,9 +5230,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -5300,7 +5243,7 @@ "privacy-test-pages.site" ] }, - "hash": "63d2e3b00421ee3c04e28fcca1b03245" + "hash": "1f76ccef190bbe46af6766d2efcb4da3" }, "harmfulApis": { "settings": { @@ -5414,15 +5357,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "8cb27af67989404f1651e164f90b7940" + "hash": "fb598c4167ff166d85dd49c701cc5579" }, "history": { "state": "disabled", @@ -5453,14 +5393,11 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], - "hash": "8e60b4c39599560b0bea1030299fb3a7" + "hash": "b47d255c6f836ecb7ae0b3e61cc2c025" }, "incontextSignup": { "exceptions": [], @@ -5509,9 +5446,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -5524,7 +5458,7 @@ ] }, "state": "enabled", - "hash": "4877b3fce635ba78a673e87943f53091" + "hash": "d14f6e3a9aa4139ee1d517016b59691e" }, "networkProtection": { "state": "enabled", @@ -5600,15 +5534,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "596714c5b23bcdd1e0181ca2af22001d" + "hash": "82088db85ca7f64418fbfd57db25ade1" }, "performanceMetrics": { "state": "enabled", @@ -5625,14 +5556,11 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], - "hash": "a81619b928b99f1d21c9d513faf69b30" + "hash": "6792064606a5a72c5cd44addb4d40bda" }, "phishingDetection": { "state": "internal", @@ -5649,9 +5577,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -5664,7 +5589,7 @@ "state": "internal" } }, - "hash": "a2ee20806fbb71157619760407ef7623" + "hash": "9a9143022e6cc8976461b337abfa81a1" }, "pluginPointFocusedViewPlugin": { "state": "disabled", @@ -5801,15 +5726,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "9e37b1b1111a12703ba85b825a5ee6cf" + "hash": "138c3b2409f6b3bf967b804ab9bf2ce2" }, "remoteMessaging": { "state": "enabled", @@ -5831,9 +5753,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -5841,7 +5760,7 @@ "settings": { "windowInMs": 0 }, - "hash": "f37136d479956daf56bcd58e02eee96a" + "hash": "baf19d9e0f506ed09f46c95b1849adee" }, "runtimeChecks": { "state": "disabled", @@ -5858,15 +5777,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "settings": {}, - "hash": "8b903028eea1ff475f6a25035e428558" + "hash": "dfede9f06b9e322e198736703d013d15" }, "serviceworkerInitiatedRequests": { "exceptions": [ @@ -5882,15 +5798,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "4818e6983b16ddfdd078a2565bc69859" + "hash": "be6751fe0307a7e1b9476f4d8b8d0aaf" }, "sslCertificates": { "state": "enabled", @@ -6899,6 +6812,7 @@ { "rule": "eccmp.com/sts/scripts/conversen-SDK.js", "domains": [ + "citi.com", "pch.com" ] } @@ -9324,14 +9238,11 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], - "hash": "2014f5e979fe2af4295fc99f78e8b252" + "hash": "896d7fc02c7f3f523fdf48e775ce30ef" }, "trackingCookies1p": { "settings": { @@ -9353,15 +9264,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "af648fa280cea7a9d0d10c90cd704b60" + "hash": "763f56424b0827b5731927a043219912" }, "trackingCookies3p": { "settings": { @@ -9380,15 +9288,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "596714c5b23bcdd1e0181ca2af22001d" + "hash": "82088db85ca7f64418fbfd57db25ade1" }, "trackingParameters": { "exceptions": [ @@ -9407,9 +9312,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } @@ -9445,7 +9347,7 @@ }, "state": "enabled", "minSupportedVersion": "0.22.3", - "hash": "a2e7e95c0788ff99d03d0b9887416fbf" + "hash": "d4fd05626079512387bd1fc0de585cb9" }, "userAgentRotation": { "settings": { @@ -9464,15 +9366,12 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "instructure.com" } ], "state": "disabled", - "hash": "66f96a6be1339c70b110cef3abd09a98" + "hash": "9225b8785d6973db37abde99d81d219c" }, "voiceSearch": { "exceptions": [], @@ -9498,9 +9397,6 @@ { "domain": "flexmls.com" }, - { - "domain": "capitalone.com" - }, { "domain": "lastpass.com" }, @@ -9766,7 +9662,7 @@ } ] }, - "hash": "fcfba000dbe8a50227bfcfdc0c0ff90c" + "hash": "d1b128e9ff49486fb7122c70fef0ff51" }, "webViewBlobDownload": { "exceptions": [], @@ -9803,6 +9699,11 @@ "state": "disabled", "hash": "728493ef7a1488e4781656d3f9db84aa" }, + "windowsWebviewFailures": { + "exceptions": [], + "state": "disabled", + "hash": "728493ef7a1488e4781656d3f9db84aa" + }, "windowsDownloadLink": { "exceptions": [], "state": "disabled", From 14b32324db0af391235c84b0eb63be1722d1ad36 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 2 Sep 2024 12:37:34 +0000 Subject: [PATCH 10/16] Set marketing version to 1.105.0 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 5f34e807bc..7510b879b0 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.104.0 +MARKETING_VERSION = 1.105.0 From 51393378f441bee3b51756d504b40153ef820671 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 2 Sep 2024 13:13:17 +0000 Subject: [PATCH 11/16] Bump version to 1.105.0 (252) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 527880d7b3..27a1c8bc72 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 251 +CURRENT_PROJECT_VERSION = 252 From 432d14bd954862d42ebe8ee8db19b40432089fa0 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 2 Sep 2024 21:19:26 +0500 Subject: [PATCH 12/16] Fix bookmarks bar issues (#3187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1201048563534612/1208204239267609/f https://app.asana.com/0/1201048563534612/1208160187803231/f Tech Design URL: CC: **Description**: - Fix "New Folder" button not working in Bookmarks Popover - Fix bookmarks popover not closed when dragging a bookmark out of it - Fix Esc key not closing "New Bookmark" dialog - Fix couple warnings **Steps to test this PR**: 1. Validate New Folder button works in the Bookmarks Popover 2. Drag a bookmark from the Bookmarks Popover over a Bookmarks Bar folder, validate the popover is closed 3. Click "Add new bookmark", hit Esc key, validate the dialog is closed 4. Add a bookmark, click the "bookmark" button in the address bar, validate Esc key doesn‘t delete the bookmark **Definition of Done**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo/Bookmarks/Services/BookmarksContextMenu.swift | 2 +- DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift | 1 + DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift | 7 +++---- .../Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift | 1 + DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift | 2 ++ DuckDuckGo/BookmarksBar/View/BookmarksBarMenuPopover.swift | 4 ++-- DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift | 2 +- DuckDuckGo/Tab/View/BrowserTabViewController.swift | 2 +- 8 files changed, 12 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo/Bookmarks/Services/BookmarksContextMenu.swift b/DuckDuckGo/Bookmarks/Services/BookmarksContextMenu.swift index 24e6b3ef28..18902d8122 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarksContextMenu.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarksContextMenu.swift @@ -143,7 +143,7 @@ extension BookmarksContextMenu { static func menuItems(for folder: BookmarkFolder, target: AnyObject?, forSearch: Bool, includeManageBookmarksItem: Bool) -> [NSMenuItem] { // disable "Open All" if no Bookmarks in folder - var hasBookmarks = folder.children.contains(where: { $0 is Bookmark }) + let hasBookmarks = folder.children.contains(where: { $0 is Bookmark }) var items = [ openInNewTabsMenuItem(folder: folder, target: target, enabled: hasBookmarks), openAllInNewWindowMenuItem(folder: folder, target: target, enabled: hasBookmarks), diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift index bcef29a4c5..2783fe14b4 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift @@ -51,6 +51,7 @@ struct AddBookmarkPopoverView: View { otherActionTitle: UserText.delete, isOtherActionDisabled: false, otherAction: model.removeButtonAction, + isOtherActionTriggeredByEscKey: false, defaultActionTitle: UserText.done, isDefaultActionDisabled: model.isDefaultActionButtonDisabled, defaultAction: model.doneButtonAction diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index 208e47b3ec..234404402c 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -158,6 +158,9 @@ final class BookmarkListViewController: NSViewController { boxDivider.setContentHuggingPriority(.defaultHigh, for: .vertical) boxDivider.translatesAutoresizingMaskIntoConstraints = false + // keep OutlineView menu declaration before buttons as it‘s used as target + outlineView.menu = BookmarksContextMenu(bookmarkManager: bookmarkManager, delegate: self) + stackView.orientation = .horizontal stackView.spacing = 4 stackView.setHuggingPriority(.defaultHigh, for: .horizontal) @@ -170,9 +173,6 @@ final class BookmarkListViewController: NSViewController { stackView.addArrangedSubview(buttonsDivider) stackView.addArrangedSubview(manageBookmarksButton) - // keep OutlineView menu declaration before the buttons as it‘s their target - outlineView.menu = BookmarksContextMenu(bookmarkManager: bookmarkManager, delegate: self) - newBookmarkButton.bezelStyle = .shadowlessSquare newBookmarkButton.cornerRadius = 4 newBookmarkButton.normalTintColor = .button @@ -254,7 +254,6 @@ final class BookmarkListViewController: NSViewController { outlineView.usesAutomaticRowHeights = true outlineView.target = self outlineView.action = #selector(handleClick) - outlineView.menu = BookmarksContextMenu(bookmarkManager: bookmarkManager, delegate: self) outlineView.dataSource = dataSource outlineView.delegate = dataSource diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift index 78cdc6efcd..114ecb6858 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift @@ -51,6 +51,7 @@ struct AddEditBookmarkDialogView: ModalView { otherActionTitle: viewModel.bookmarkModel.cancelActionTitle, isOtherActionDisabled: viewModel.bookmarkModel.isOtherActionDisabled, otherAction: viewModel.bookmarkModel.cancel, + isOtherActionTriggeredByEscKey: true, defaultActionTitle: viewModel.bookmarkModel.defaultActionTitle, isDefaultActionDisabled: viewModel.bookmarkModel.isDefaultActionDisabled, defaultAction: viewModel.bookmarkModel.addOrSave diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift index 8d34889432..7831248391 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift @@ -36,6 +36,7 @@ struct AddEditBookmarkView: View { let otherActionTitle: String let isOtherActionDisabled: Bool let otherAction: @MainActor (_ dismiss: () -> Void) -> Void + let isOtherActionTriggeredByEscKey: Bool let defaultActionTitle: String let isDefaultActionDisabled: Bool @@ -78,6 +79,7 @@ struct AddEditBookmarkView: View { viewState: .init(buttonsState), otherButtonAction: .init( title: otherActionTitle, + keyboardShortCut: isOtherActionTriggeredByEscKey ? .cancelAction : nil, isDisabled: isOtherActionDisabled, action: otherAction ), diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuPopover.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuPopover.swift index 2de6a55def..5da88072c1 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuPopover.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuPopover.swift @@ -113,12 +113,12 @@ final class BookmarksBarMenuPopover: NSPopover { return frame } - /// close other BookmarkListPopover-s shown from the main window when opening a new one + /// close other `BookmarksBarMenuPopover`-s and `BookmarkListPopover`-s shown from the main window when opening a new one static func closeBookmarkListPopovers(shownIn window: NSWindow?, except popoverToKeep: BookmarksBarMenuPopover? = nil) { guard let window, // ignore when opening a submenu from another BookmarkListPopover !(window.contentViewController?.nextResponder is Self) else { return } - for case let .some(popover as Self) in (window.childWindows ?? []).map(\.contentViewController?.nextResponder) where popover !== popoverToKeep && popover.isShown { + for case let .some(popover as NSPopover) in (window.childWindows ?? []).map(\.contentViewController?.nextResponder) where popover !== popoverToKeep && popover.isShown { popover.close() } } diff --git a/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift index 21f8dc1cc2..65a9134400 100644 --- a/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift @@ -362,7 +362,7 @@ extension DuckPlayerTabExtension: NavigationResponder { return } if navigation.url.isDuckPlayer { - var setting = preferences.duckPlayerMode == .enabled ? "always" : "default" + let setting = preferences.duckPlayerMode == .enabled ? "always" : "default" let newTabSettings = preferences.duckPlayerOpenInNewTab ? "true" : "false" let autoplay = preferences.duckPlayerAutoplay ? "true" : "false" diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 0d1f226caf..d1e20e9248 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -439,7 +439,7 @@ final class BrowserTabViewController: NSViewController { private func subscribeToDuckPlayerOnboardingPrompt(of tabViewModel: TabViewModel?) { tabViewModel?.tab.duckPlayerOnboardingPublisher.sink { [weak self, weak tab = tabViewModel?.tab] onboardingState in - guard let self, let tab, let onboardingState = onboardingState, onboardingState.onboardingDecider.canDisplayOnboarding else { + guard let self, tab != nil, let onboardingState = onboardingState, onboardingState.onboardingDecider.canDisplayOnboarding else { self?.duckPlayerOnboardingModalManager.close(animated: false, completion: nil) return } From 3eb48b60664a23c56eb5f4c07dea7fbc0afaac24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Mon, 2 Sep 2024 19:45:23 +0200 Subject: [PATCH 13/16] Remote feature flag for New Tab Page Improvements (#3176) Task/Issue URL: https://app.asana.com/0/72649045549333/1207539163549338/f Tech Design URL: CC: **Description**: No functional/code changes in macOS, only BSK reference update. **Steps to test this PR**: 1. Make sure the project builds. **Definition of Done**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d58b3a866f..8acbf914c9 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13740,7 +13740,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 191.0.1; + version = 191.1.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 51adade877..66577b58f8 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "086d8e74c85783bf27df03f5ac2586a910d69155", - "version" : "191.0.1" + "revision" : "afdab422a36d6ab20ab08a407927f0e50d683ff5", + "version" : "191.1.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index dc3ba2d552..e0c25310e2 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: "191.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "191.1.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 71ae77642e..1a4855c3bd 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: "191.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "191.1.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 d35e2fea45..3484bafa20 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: "191.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "191.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From f6ef398f23ef4e6a91ed62a860b42fd02ac2ab6d Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Tue, 3 Sep 2024 05:35:49 +0000 Subject: [PATCH 14/16] Bump version to 1.105.0 (253) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 27a1c8bc72..a2a5610bdd 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 252 +CURRENT_PROJECT_VERSION = 253 From 4e6c672381013ee7c4e7e670698d04c34bc1c200 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 3 Sep 2024 10:11:38 -0300 Subject: [PATCH 15/16] Bug: Disable boomark reordering when searching (#3188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1204006570077678/1208204239267626/f Tech Design URL: CC: **Description**: - Fix a bug where bookmark reordering was possible during search (both in the panel and the manager) - Fix a bug where the folder was not correctly highlighted after being tapped in the bookmarks panel **Steps to test this PR**: 1. Start a search (either in the bookmarks manager o panel) 2. Check that you cannot reorder items 3. You should be able to drop items in a folder **Definition of Done**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? — ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../Bookmarks/Model/BookmarkOutlineViewDataSource.swift | 7 +++++++ DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift | 2 +- .../View/BookmarkManagementDetailViewController.swift | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift index c80c02f163..9a6d7b840c 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift @@ -264,6 +264,13 @@ final class BookmarkOutlineViewDataSource: NSObject, BookmarksOutlineViewDataSou } let destination = destinationNode.isRoot ? PseudoFolder.bookmarks : destinationNode.representedObject + + guard !isSearching || destination is BookmarkFolder else { return .none } + + if let destinationFolder = destination as? BookmarkFolder { + self.dragDestinationFolder = destinationFolder + } + let operation = dragDropManager.validateDrop(info, to: destination) self.dragDestinationFolder = (operation == .none || item == nil) ? nil : destinationNode.representedObject as? BookmarkFolder diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index 234404402c..4b4bd45bef 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -451,7 +451,7 @@ final class BookmarkListViewController: NSViewController { expandFoldersAndScrollUntil(folder) outlineView.scrollToAdjustedPositionInOutlineView(folder) - guard let node = treeController.node(representing: folder) else { return } + guard let node = treeController.findNodeWithId(representing: folder) else { return } outlineView.highlight(node) } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift index f857da6728..cc251ff4d6 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift @@ -473,6 +473,9 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation { let destination = destination(for: dropOperation, at: row) + + guard !isSearching || destination is BookmarkFolder else { return .none } + return dragDropManager.validateDrop(info, to: destination) } From 8561bd2a71309f540e75a340c1373cee65d003ef Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Tue, 3 Sep 2024 10:59:56 -0300 Subject: [PATCH 16/16] Add bookmarks search UI tests (#3161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1204006570077678/1208027756712863/f Tech Design URL: CC: ## Description Adds the following UI tests: - Test 'No results' empty state is shown when searching and no results (both panel and manager) - Test that searching filter the correct results (both panel and manager) - Test 'Show in Folder' menu item action is shown on items when searching (both panel and manager) - Test search actions should be hidden when the user has no bookmarks (panel) ## Steps to test 1. Selec the `UI Tests` target 2. Run the tests inside `BookmarkSearchTests` **Definition of Done**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? — ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --------- Co-authored-by: Dominik Kapusta --- DuckDuckGo.xcodeproj/project.pbxproj | 8 + .../View/BookmarkListViewController.swift | 6 + ...okmarkManagementDetailViewController.swift | 4 + .../BookmarksEmptyStateContent.swift | 12 + DuckDuckGo/Menus/MainMenu.swift | 40 ++- DuckDuckGo/Menus/MainMenuActions.swift | 1 + .../View/NavigationBarViewController.swift | 1 + UITests/BookmarkSearchTests.swift | 334 ++++++++++++++++++ UITests/Common/XCUIApplicationExtension.swift | 108 ++++++ 9 files changed, 495 insertions(+), 19 deletions(-) create mode 100644 UITests/BookmarkSearchTests.swift create mode 100644 UITests/Common/XCUIApplicationExtension.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 8acbf914c9..4d5b1a0e8b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2584,10 +2584,12 @@ BB7B5F992C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */; }; BBB881882C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */; }; BBB881892C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */; }; + BBBB65402C77BB9400E69AC6 /* BookmarkSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBBB653F2C77BB9400E69AC6 /* BookmarkSearchTests.swift */; }; BBBEE1BF2C4FF63600035ABA /* SortBookmarksViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBBEE1BE2C4FF63600035ABA /* SortBookmarksViewModelTests.swift */; }; BBBEE1C02C4FF63600035ABA /* SortBookmarksViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBBEE1BE2C4FF63600035ABA /* SortBookmarksViewModelTests.swift */; }; BBC063E82C5A9E4B007BDC18 /* BookmarkManagementDetailViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBC063E72C5A9E4B007BDC18 /* BookmarkManagementDetailViewModelTests.swift */; }; BBC063E92C5A9E4B007BDC18 /* BookmarkManagementDetailViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBC063E72C5A9E4B007BDC18 /* BookmarkManagementDetailViewModelTests.swift */; }; + BBCD467A2C8643EC004DB483 /* XCUIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBCD46792C8643EC004DB483 /* XCUIApplicationExtension.swift */; }; BBE013EA2C5BFD660025F2C6 /* BookmarksEmptyStateContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBE013E92C5BFD660025F2C6 /* BookmarksEmptyStateContent.swift */; }; BBE013EB2C5BFD660025F2C6 /* BookmarksEmptyStateContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBE013E92C5BFD660025F2C6 /* BookmarksEmptyStateContent.swift */; }; BBFB727F2C48047C0088884C /* SortBookmarksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBFB727E2C48047C0088884C /* SortBookmarksViewModel.swift */; }; @@ -4331,8 +4333,10 @@ BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionSubscriptionEventHandler.swift; sourceTree = ""; }; BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksSearchAndSortMetrics.swift; sourceTree = ""; }; BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkListTreeControllerSearchDataSource.swift; sourceTree = ""; }; + BBBB653F2C77BB9400E69AC6 /* BookmarkSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSearchTests.swift; sourceTree = ""; }; BBBEE1BE2C4FF63600035ABA /* SortBookmarksViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBookmarksViewModelTests.swift; sourceTree = ""; }; BBC063E72C5A9E4B007BDC18 /* BookmarkManagementDetailViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkManagementDetailViewModelTests.swift; sourceTree = ""; }; + BBCD46792C8643EC004DB483 /* XCUIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIApplicationExtension.swift; sourceTree = ""; }; BBE013E92C5BFD660025F2C6 /* BookmarksEmptyStateContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksEmptyStateContent.swift; sourceTree = ""; }; BBFB727E2C48047C0088884C /* SortBookmarksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBookmarksViewModel.swift; sourceTree = ""; }; BBFF355C2C4AF26200DA3289 /* BookmarksSortModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksSortModeTests.swift; sourceTree = ""; }; @@ -6310,6 +6314,7 @@ EE9D81C22BC57A3700338BE3 /* StateRestorationTests.swift */, 7B4CE8E626F02134009134B1 /* TabBarTests.swift */, 56A054522C2592CE007D8FAB /* OnboardingUITests.swift */, + BBBB653F2C77BB9400E69AC6 /* BookmarkSearchTests.swift */, ); path = UITests; sourceTree = ""; @@ -8742,6 +8747,7 @@ children = ( EE02D4192BB4609900DBE6B3 /* UITests.swift */, EEBCE6812BA444FA00B9DF00 /* XCUIElementExtension.swift */, + BBCD46792C8643EC004DB483 /* XCUIApplicationExtension.swift */, ); path = Common; sourceTree = ""; @@ -11506,7 +11512,9 @@ EE02D41C2BB460A600DBE6B3 /* BrowsingHistoryTests.swift in Sources */, EE02D41A2BB4609900DBE6B3 /* UITests.swift in Sources */, EE0429E02BA31D2F009EB20F /* FindInPageTests.swift in Sources */, + BBCD467A2C8643EC004DB483 /* XCUIApplicationExtension.swift in Sources */, EE02D4212BB460FE00DBE6B3 /* StringExtension.swift in Sources */, + BBBB65402C77BB9400E69AC6 /* BookmarkSearchTests.swift in Sources */, 56A054532C2592CE007D8FAB /* OnboardingUITests.swift in Sources */, EE9D81C32BC57A3700338BE3 /* StateRestorationTests.swift in Sources */, EEC7BE2E2BC6C09500F86835 /* AddressBarKeyboardShortcutsTests.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index 4b4bd45bef..fb670ee326 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -41,7 +41,9 @@ final class BookmarkListViewController: NSViewController { private lazy var stackView = NSStackView() private lazy var newBookmarkButton = MouseOverButton(image: .addBookmark, target: self, action: #selector(newBookmarkButtonClicked)) private lazy var newFolderButton = MouseOverButton(image: .addFolder, target: outlineView.menu, action: #selector(FolderMenuItemSelectors.newFolder)) + .withAccessibilityIdentifier("BookmarkListViewController.newFolderButton") private lazy var searchBookmarksButton = MouseOverButton(image: .searchBookmarks, target: self, action: #selector(searchBookmarkButtonClicked)) + .withAccessibilityIdentifier("BookmarkListViewController.searchBookmarksButton") private lazy var sortBookmarksButton = MouseOverButton(image: .bookmarkSortAsc, target: self, action: #selector(sortBookmarksButtonClicked)) private lazy var buttonsDivider = NSBox() @@ -53,10 +55,14 @@ final class BookmarkListViewController: NSViewController { private lazy var emptyState = NSView() private lazy var emptyStateTitle = NSTextField() + .withAccessibilityIdentifier(BookmarksEmptyStateContent.titleAccessibilityIdentifier) private lazy var emptyStateMessage = NSTextField() + .withAccessibilityIdentifier(BookmarksEmptyStateContent.descriptionAccessibilityIdentifier) private lazy var emptyStateImageView = NSImageView(image: .bookmarksEmpty) + .withAccessibilityIdentifier(BookmarksEmptyStateContent.imageAccessibilityIdentifier) private lazy var importButton = NSButton(title: UserText.importBookmarksButtonTitle, target: self, action: #selector(onImportClicked)) private lazy var searchBar = NSSearchField() + .withAccessibilityIdentifier("BookmarkListViewController.searchBar") private var boxDividerTopConstraint = NSLayoutConstraint() private let bookmarkManager: BookmarkManager diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift index cc251ff4d6..8bcfe5d45a 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift @@ -45,14 +45,18 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem .withAccessibilityIdentifier("BookmarkManagementDetailViewController.sortItemsButton") lazy var searchBar = NSSearchField() + .withAccessibilityIdentifier("BookmarkManagementDetailViewController.searchBar") private lazy var separator = NSBox() private lazy var scrollView = NSScrollView() private lazy var tableView = NSTableView() private lazy var emptyState = NSView() private lazy var emptyStateImageView = NSImageView(image: .bookmarksEmpty) + .withAccessibilityIdentifier(BookmarksEmptyStateContent.imageAccessibilityIdentifier) private lazy var emptyStateTitle = NSTextField() + .withAccessibilityIdentifier(BookmarksEmptyStateContent.titleAccessibilityIdentifier) private lazy var emptyStateMessage = NSTextField() + .withAccessibilityIdentifier(BookmarksEmptyStateContent.descriptionAccessibilityIdentifier) private lazy var importButton = NSButton(title: UserText.importBookmarksButtonTitle, target: self, action: #selector(onImportClicked)) weak var delegate: BookmarkManagementDetailViewControllerDelegate? diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarksEmptyStateContent.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarksEmptyStateContent.swift index 3a32c9c2ce..acbb8ee4ac 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/BookmarksEmptyStateContent.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarksEmptyStateContent.swift @@ -20,6 +20,18 @@ enum BookmarksEmptyStateContent { case noBookmarks case noSearchResults + static var titleAccessibilityIdentifier: String { + "BookmarksEmptyStateContent.emptyStateTitle" + } + + static var descriptionAccessibilityIdentifier: String { + "BookmarksEmptyStateContent.emptyStateMessage" + } + + static var imageAccessibilityIdentifier: String { + "BookmarksEmptyStateContent.emptyStateImageView" + } + var title: String { switch self { case .noBookmarks: return UserText.bookmarksEmptyStateTitle diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index d03027637c..0b47323d86 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -290,28 +290,30 @@ final class MainMenu: NSMenu { } func buildBookmarksMenu() -> NSMenuItem { - NSMenuItem(title: UserText.bookmarks).submenu(bookmarksMenu.buildItems { - NSMenuItem(title: UserText.bookmarkThisPage, action: #selector(MainViewController.bookmarkThisPage), keyEquivalent: "d") - NSMenuItem(title: UserText.bookmarkAllTabs, action: #selector(MainViewController.bookmarkAllOpenTabs), keyEquivalent: [.command, .shift, "d"]) - manageBookmarksMenuItem - bookmarksMenuToggleBookmarksBarMenuItem - NSMenuItem.separator() + NSMenuItem(title: UserText.bookmarks) + .withAccessibilityIdentifier("MainMenu.bookmarks") + .submenu(bookmarksMenu.buildItems { + NSMenuItem(title: UserText.bookmarkThisPage, action: #selector(MainViewController.bookmarkThisPage), keyEquivalent: "d") + NSMenuItem(title: UserText.bookmarkAllTabs, action: #selector(MainViewController.bookmarkAllOpenTabs), keyEquivalent: [.command, .shift, "d"]) + manageBookmarksMenuItem + bookmarksMenuToggleBookmarksBarMenuItem + NSMenuItem.separator() - importBookmarksMenuItem - NSMenuItem(title: UserText.exportBookmarks, action: #selector(AppDelegate.openExportBookmarks)) - NSMenuItem.separator() + importBookmarksMenuItem + NSMenuItem(title: UserText.exportBookmarks, action: #selector(AppDelegate.openExportBookmarks)) + NSMenuItem.separator() - NSMenuItem(title: UserText.favorites) - .submenu(favoritesMenu.buildItems { - NSMenuItem(title: UserText.mainMenuHistoryFavoriteThisPage, action: #selector(MainViewController.favoriteThisPage)) - .withImage(.favorite) - .withAccessibilityIdentifier("MainMenu.favoriteThisPage") - NSMenuItem.separator() - }) - .withImage(.favorite) + NSMenuItem(title: UserText.favorites) + .submenu(favoritesMenu.buildItems { + NSMenuItem(title: UserText.mainMenuHistoryFavoriteThisPage, action: #selector(MainViewController.favoriteThisPage)) + .withImage(.favorite) + .withAccessibilityIdentifier("MainMenu.favoriteThisPage") + NSMenuItem.separator() + }) + .withImage(.favorite) - NSMenuItem.separator() - }) + NSMenuItem.separator() + }) } func buildWindowMenu() -> NSMenuItem { diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 7d91327dc4..70371865b5 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -803,6 +803,7 @@ extension MainViewController { @objc func resetBookmarks(_ sender: Any?) { LocalBookmarkManager.shared.resetBookmarks() UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.homePageContinueSetUpImport.rawValue) + UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.bookmarksBarPromptShown.rawValue) } @objc func resetPinnedTabs(_ sender: Any?) { diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 2a16c707e8..4aac241e6f 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -169,6 +169,7 @@ final class NavigationBarViewController: NSViewController { bookmarkListButton.sendAction(on: .leftMouseDown) bookmarkListButton.registerForDraggedTypes(BookmarkDragDropManager.draggedTypes) bookmarkListButton.delegate = self + bookmarkListButton.setAccessibilityIdentifier("NavigationBarViewController.bookmarkListButton") downloadsButton.sendAction(on: .leftMouseDown) networkProtectionButton.sendAction(on: .leftMouseDown) passwordManagementButton.sendAction(on: .leftMouseDown) diff --git a/UITests/BookmarkSearchTests.swift b/UITests/BookmarkSearchTests.swift new file mode 100644 index 0000000000..79f2395ddf --- /dev/null +++ b/UITests/BookmarkSearchTests.swift @@ -0,0 +1,334 @@ +// +// BookmarkSearchTests.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 XCTest + +class BookmarkSearchTests: XCTestCase { + private var app: XCUIApplication! + + private enum AccessibilityIdentifiers { + static let bookmarkButton = "AddressBarButtonsViewController.bookmarkButton" + static let addressBarTextField = "AddressBarViewController.addressBarTextField" + static let manageBookmarksMenuItem = "MainMenu.manageBookmarksMenuItem" + static let bookmarksMenu = "MainMenu.bookmarks" + static let bookmarksPanelShortcutButton = "NavigationBarViewController.bookmarkListButton" + static let optionsButton = "NavigationBarViewController.optionsButton" + static let resetBookmarksMenuItem = "MainMenu.resetBookmarks" + static let searchBookmarksButton = "BookmarkListViewController.searchBookmarksButton" + static let bookmarksPanelSearchBar = "BookmarkListViewController.searchBar" + static let bookmarksManagerSearchBar = "BookmarkManagementDetailViewController.searchBar" + static let emptyStateTitle = "BookmarksEmptyStateContent.emptyStateTitle" + static let emptyStateMessage = "BookmarksEmptyStateContent.emptyStateMessage" + static let emptyStateImageView = "BookmarksEmptyStateContent.emptyStateImageView" + static let newFolderButton = "BookmarkListViewController.newFolderButton" + } + + override class func setUp() { + UITests.firstRun() + } + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchEnvironment["UITEST_MODE"] = "1" + app.launch() + app.resetBookmarks() + enforceSingleWindow() + } + + // MARK: - Tests + + func testEmptyStateWhenSearchingInPanel() { + addBookmarkAndOpenBookmarksPanel(bookmarkPageTitle: "Bookmark #1") + verifyEmptyState(in: app.popovers.firstMatch, with: AccessibilityIdentifiers.bookmarksPanelSearchBar, mode: .panel) + } + + func testEmptyStateWhenSearchingInManager() { + addBookmarkAndOpenBookmarksManager(bookmarkPageTitle: "Bookmark #1") + verifyEmptyState(in: app, with: AccessibilityIdentifiers.bookmarksManagerSearchBar, mode: .manager) + } + + func testFilteredResultsInPanel() { + addThreeBookmarks() + closeShowBookmarksBarAlert() + openBookmarksPanel() + searchInBookmarksPanel(for: "Bookmark #2") + assertOnlyBookmarkExists(on: app.outlines.firstMatch, bookmarkTitle: "Bookmark #2") + } + + func testFilteredResultsInManager() { + addThreeBookmarks() + openBookmarksManager() + searchInBookmarksManager(for: "Bookmark #2") + assertOnlyBookmarkExists(on: app.tables.firstMatch, bookmarkTitle: "Bookmark #2") + } + + func testShowInFolderFunctionalityOnBookmarksPanel() { + testShowInFolderFunctionality(in: .panel) + } + + func testShowInFolderFunctionalityOnBookmarksManager() { + testShowInFolderFunctionality(in: .manager) + } + + func testDragAndDropToReorderIsNotPossibleWhenInSearchOnBookmarksPanel() { + testDragAndDropToReorder(in: .panel) + } + + func testDragAndDropToReorderIsNotPossibleWhenInSearchOnBookmarksManager() { + testDragAndDropToReorder(in: .manager) + } + + func testSearchActionIsHiddenOnBookmarksPanelWhenUserHasNoBookmarks() { + openBookmarksPanel() + let bookmarksPanelPopover = app.popovers.firstMatch + XCTAssertFalse(bookmarksPanelPopover.buttons[AccessibilityIdentifiers.searchBookmarksButton].exists) + } + + // MARK: - Utilities + + private func enforceSingleWindow() { + app.typeKey("w", modifierFlags: [.command, .option, .shift]) + app.typeKey("n", modifierFlags: .command) + } + + private func addBookmarkAndOpenBookmarksPanel(bookmarkPageTitle: String, in folder: String? = nil) { + addBookmark(pageTitle: bookmarkPageTitle, in: folder) + closeShowBookmarksBarAlert() + openBookmarksPanel() + } + + private func addBookmarkAndOpenBookmarksManager(bookmarkPageTitle: String, in folder: String? = nil) { + addBookmark(pageTitle: bookmarkPageTitle, in: folder) + openBookmarksManager() + } + + private func addThreeBookmarks() { + ["Bookmark #1", "Bookmark #2", "Bookmark #3"].forEach { + addBookmark(pageTitle: $0) + openNewTab() + } + } + + private func addBookmark(pageTitle: String, in folder: String? = nil) { + let urlForBookmarksBar = UITests.simpleServedPage(titled: pageTitle) + app.openSiteToBookmark(app: app, + url: urlForBookmarksBar, + pageTitle: pageTitle, + bookmarkingViaDialog: true, + escapingDialog: true, + folderName: folder) + } + + private func searchInBookmarksPanel(for title: String) { + bringFocusToBookmarksPanelSearchBar() + app.popovers.firstMatch.searchFields[AccessibilityIdentifiers.bookmarksPanelSearchBar].typeText(title) + } + + private func searchInBookmarksManager(for title: String) { + let searchField = app.searchFields[AccessibilityIdentifiers.bookmarksManagerSearchBar] + searchField.tap() + searchField.typeText(title) + } + + private func assertOnlyBookmarkExists(on element: XCUIElement, bookmarkTitle: String) { + XCTAssertTrue(element.staticTexts[bookmarkTitle].exists) + // Assert that other bookmarks do not exist + ["Bookmark #1", "Bookmark #2", "Bookmark #3"].filter { $0 != bookmarkTitle }.forEach { + XCTAssertFalse(element.staticTexts[$0].exists) + } + } + + private func verifyEmptyState(in element: XCUIElement, with accessibilityIdentifier: String, mode: BookmarkMode) { + if mode == .panel { + searchInBookmarksPanel(for: "No results") + } else { + searchInBookmarksManager(for: "No results") + } + assertEmptyState(in: element) + } + + private func assertEmptyState(in element: XCUIElement) { + let emptyStateTitle = element.staticTexts[AccessibilityIdentifiers.emptyStateTitle] + let emptyStateDescription = element.staticTexts[AccessibilityIdentifiers.emptyStateMessage] + let emptyStateImage = element.images[AccessibilityIdentifiers.emptyStateImageView] + + XCTAssertTrue(emptyStateImage.exists, "The empty state image does not exist.") + XCTAssertTrue(emptyStateTitle.exists, "The empty state title does not exist.") + XCTAssertTrue(emptyStateDescription.exists, "The empty state description does not exist.") + + XCTAssertEqual(emptyStateTitle.value as? String, "No bookmarks found") + XCTAssertEqual(emptyStateDescription.value as? String, "Try different search terms.") + } + + private func bringFocusToBookmarksPanelSearchBar() { + let popover = app.popovers.firstMatch + popover.buttons[AccessibilityIdentifiers.searchBookmarksButton].tap() + } + + private func openBookmarksPanel() { + app.showAndTapBookmarksPanelShortcut() + } + + private func openBookmarksManager() { + app.openBookmarksManager() + } + + private func openNewTab() { + app.typeKey("t", modifierFlags: .command) + } + + private func testShowInFolderFunctionality(in mode: BookmarkMode) { + createFolderWithSubFolder() + openNewTab() + addBookmark(pageTitle: "Bookmark #1", in: "Folder #2") + closeShowBookmarksBarAlert() + + if mode == .panel { + openBookmarksPanel() + searchInBookmarksPanel(for: "Bookmark #1") + } else { + openBookmarksManager() + searchInBookmarksManager(for: "Bookmark #1") + } + + let result = app.staticTexts["Bookmark #1"] + result.rightClick() + let showInFolderMenuItem = app.menuItems["Show in Folder"] + XCTAssertTrue(showInFolderMenuItem.exists) + showInFolderMenuItem.tap() + + assertSearchBarVisibilityAfterShowInFolder(mode: mode) + assertFolderStructure(mode: mode) + } + + private func assertSearchBarVisibilityAfterShowInFolder(mode: BookmarkMode) { + if mode == .panel { + XCTAssertFalse(app.popovers.firstMatch.searchFields[AccessibilityIdentifiers.bookmarksPanelSearchBar].exists) + } else { + XCTAssertEqual(app.searchFields[AccessibilityIdentifiers.bookmarksManagerSearchBar].value as? String, "") + } + } + + private func assertFolderStructure(mode: BookmarkMode) { + let treeBookmarks: XCUIElement = mode == .panel ? app.popovers.firstMatch.outlines.firstMatch : app.outlines.firstMatch + + XCTAssertTrue(treeBookmarks.staticTexts["Folder #1"].exists) + if mode == .panel { + XCTAssertTrue(treeBookmarks.staticTexts["Bookmark #1"].exists) + XCTAssertTrue(treeBookmarks.staticTexts["Folder #2"].exists) + } else { + /// On the bookmarks manager the sidebar tree structure only has folders while the list has what's inside the selected folder in the tree. + XCTAssertTrue(treeBookmarks.staticTexts["Folder #2"].exists) + let bookmarksList = app.tables.firstMatch + XCTAssertTrue(bookmarksList.staticTexts["Bookmark #1"].exists) + } + } + + private func testDragAndDropToReorder(in mode: BookmarkMode) { + addThreeBookmarks() + if mode == .panel { + closeShowBookmarksBarAlert() + openBookmarksPanel() + } else { + openBookmarksManager() + } + searchInBookmarks(mode: mode) + + let thirdBookmarkCell = getThirdBookmarkCell(mode: mode) + dragAndDropBookmark(thirdBookmarkCell, mode: mode) + + if mode == .panel { + bringFocusToBookmarksPanelSearchBar() + } else { + clearSearchInBookmarksManager() + } + + verifyBookmarkOrder(expectedOrder: ["Bookmark #1", "Bookmark #2", "Bookmark #3"], mode: mode) + } + + private func searchInBookmarks(mode: BookmarkMode) { + if mode == .panel { + searchInBookmarksPanel(for: "Bookmark") + } else { + searchInBookmarksManager(for: "Bookmark") + } + } + + private func getThirdBookmarkCell(mode: BookmarkMode) -> XCUIElement { + if mode == .panel { + let treeBookmarks = app.popovers.firstMatch.outlines.firstMatch + return treeBookmarks.staticTexts["Bookmark #3"] + } else { + let bookmarksSearchResultsList = app.tables.firstMatch + return bookmarksSearchResultsList.staticTexts["Bookmark #3"] + } + } + + private func dragAndDropBookmark(_ thirdBookmarkCell: XCUIElement, mode: BookmarkMode) { + let startCoordinate = thirdBookmarkCell.coordinate(withNormalizedOffset: .zero) + let targetCoordinate = (mode == .panel ? app.popovers.firstMatch.outlines.firstMatch : app.tables.firstMatch).coordinate(withNormalizedOffset: .zero) + startCoordinate.press(forDuration: 0.1, thenDragTo: targetCoordinate) + } + + private func clearSearchInBookmarksManager() { + let searchField = app.searchFields[AccessibilityIdentifiers.bookmarksManagerSearchBar] + searchField.doubleTap() + app.typeKey(.delete, modifierFlags: []) + } + + private func verifyBookmarkOrder(expectedOrder: [String], mode: BookmarkMode) { + let rowCount = (mode == .panel ? app.popovers.firstMatch.outlines.firstMatch : app.tables.firstMatch).cells.count + XCTAssertEqual(rowCount, expectedOrder.count, "Row count does not match expected count.") + + for index in 0..