From 5f20c6c8519bfb666f5637e11ec0f4b26ce0f404 Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Tue, 21 May 2024 10:54:31 +0200 Subject: [PATCH] Adding to the Dock automatically (#2722) Task/Issue URL: https://app.asana.com/0/72649045549333/1206797051025460/f Tech Design URL: CC: **Description**: Adding to the Dock automatically during the onboarding, through the new tab page card and Settings. --- DuckDuckGo.xcodeproj/project.pbxproj | 24 + DuckDuckGo/Application/DockCustomizer.swift | 146 ++++ .../Application/DockPositionProvider.swift | 79 ++ .../Images/Dock-128.imageset/Contents.json | 12 + .../Images/Dock-128.imageset/Dock-128.pdf | Bin 0 -> 12816 bytes DuckDuckGo/Common/Localizables/UserText.swift | 11 + .../Utilities/UserDefaultsWrapper.swift | 1 + .../Model/HomePageContinueSetUpModel.swift | 105 ++- .../HomePage/View/ContinueSetUpView.swift | 35 +- .../View/HomePageViewController.swift | 1 + DuckDuckGo/Localizable.xcstrings | 678 +++++++++++++++++- .../Onboarding/View/OnboardingFlow.swift | 17 +- .../ViewModel/OnboardingViewModel.swift | 44 ++ .../Model/DefaultBrowserPreferences.swift | 7 +- .../View/PreferencesGeneralView.swift | 48 +- .../View/PreferencesRootView.swift | 3 +- .../Statistics/ATB/StatisticsLoader.swift | 10 + DuckDuckGo/Statistics/GeneralPixel.swift | 19 + .../Tab/View/BrowserTabViewController.swift | 6 + UnitTests/App/DockCustomizerMock.swift | 39 + UnitTests/App/DockPositionProviderTests.swift | 48 ++ .../HomePage/ContinueSetUpModelTests.swift | 38 +- .../CapturingDefaultBrowserProvider.swift | 2 + UnitTests/Onboarding/OnboardingTests.swift | 41 ++ .../DefaultBrowserPreferencesTests.swift | 2 + 25 files changed, 1378 insertions(+), 38 deletions(-) create mode 100644 DuckDuckGo/Application/DockCustomizer.swift create mode 100644 DuckDuckGo/Application/DockPositionProvider.swift create mode 100644 DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Dock-128.pdf create mode 100644 UnitTests/App/DockCustomizerMock.swift create mode 100644 UnitTests/App/DockPositionProviderTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ddff21e5e7..acd1b22af2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -60,6 +60,8 @@ 1D3B1AC22936B816006F4388 /* BWMessageIdGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3B1AC12936B816006F4388 /* BWMessageIdGeneratorTests.swift */; }; 1D3B1AC429378953006F4388 /* BWResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3B1AC329378953006F4388 /* BWResponseTests.swift */; }; 1D3B1AC62937A478006F4388 /* BWRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3B1AC52937A478006F4388 /* BWRequestTests.swift */; }; + 1D4071AE2BD64267002D4537 /* DockCustomizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4071AD2BD64266002D4537 /* DockCustomizer.swift */; }; + 1D4071AF2BD64267002D4537 /* DockCustomizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4071AD2BD64266002D4537 /* DockCustomizer.swift */; }; 1D43EB32292788C70065E5D6 /* BWEncryptionOutput.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB31292788C70065E5D6 /* BWEncryptionOutput.m */; }; 1D43EB3429297D760065E5D6 /* BWNotRespondingAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB3329297D760065E5D6 /* BWNotRespondingAlert.swift */; }; 1D43EB36292ACE690065E5D6 /* ApplicationVersionReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB35292ACE690065E5D6 /* ApplicationVersionReader.swift */; }; @@ -70,6 +72,8 @@ 1D69C553291302F200B75945 /* BWVault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D69C552291302F200B75945 /* BWVault.swift */; }; 1D6A492029CF7A490011DF74 /* NSPopoverExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6A491F29CF7A490011DF74 /* NSPopoverExtension.swift */; }; 1D6A492129CF7A490011DF74 /* NSPopoverExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6A491F29CF7A490011DF74 /* NSPopoverExtension.swift */; }; + 1D7693FF2BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */; }; + 1D7694002BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */; }; 1D77921828FDC54C00BE0210 /* FaviconReferenceCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D77921728FDC54C00BE0210 /* FaviconReferenceCacheTests.swift */; }; 1D77921A28FDC79800BE0210 /* FaviconStoringMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D77921928FDC79800BE0210 /* FaviconStoringMock.swift */; }; 1D8057C82A83CAEE00F4FED6 /* SupportedOsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8057C72A83CAEE00F4FED6 /* SupportedOsChecker.swift */; }; @@ -85,6 +89,8 @@ 1D8C2FEE2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C2FEC2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift */; }; 1D8C2FF02B70F751005E4BBD /* MockTabSnapshotStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C2FEF2B70F751005E4BBD /* MockTabSnapshotStore.swift */; }; 1D8C2FF12B70F751005E4BBD /* MockTabSnapshotStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C2FEF2B70F751005E4BBD /* MockTabSnapshotStore.swift */; }; + 1D9A37672BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A37662BD8EA8800EBC58D /* DockPositionProvider.swift */; }; + 1D9A37682BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A37662BD8EA8800EBC58D /* DockPositionProvider.swift */; }; 1D9A4E5A2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */; }; 1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */; }; 1D9FDEB72B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */; }; @@ -103,6 +109,8 @@ 1DA6D0FE2A1FF9A100540406 /* HTTPCookie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FC2A1FF9A100540406 /* HTTPCookie.swift */; }; 1DA6D1022A1FFA3700540406 /* HTTPCookieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */; }; 1DA6D1032A1FFA3B00540406 /* HTTPCookieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */; }; + 1DA860722BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */; }; + 1DA860732BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */; }; 1DB67F292B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB67F282B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift */; }; 1DB67F2A2B6FEB17003DF243 /* WebViewSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB67F282B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift */; }; 1DB67F2D2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB67F2C2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift */; }; @@ -2823,6 +2831,7 @@ 1D3B1AC12936B816006F4388 /* BWMessageIdGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWMessageIdGeneratorTests.swift; sourceTree = ""; }; 1D3B1AC329378953006F4388 /* BWResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWResponseTests.swift; sourceTree = ""; }; 1D3B1AC52937A478006F4388 /* BWRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWRequestTests.swift; sourceTree = ""; }; + 1D4071AD2BD64266002D4537 /* DockCustomizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DockCustomizer.swift; sourceTree = ""; }; 1D43EB30292788C70065E5D6 /* BWEncryptionOutput.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BWEncryptionOutput.h; sourceTree = ""; }; 1D43EB31292788C70065E5D6 /* BWEncryptionOutput.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BWEncryptionOutput.m; sourceTree = ""; }; 1D43EB3329297D760065E5D6 /* BWNotRespondingAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWNotRespondingAlert.swift; sourceTree = ""; }; @@ -2833,6 +2842,7 @@ 1D6216B129069BBF00386B2C /* BWKeyStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWKeyStorage.swift; sourceTree = ""; }; 1D69C552291302F200B75945 /* BWVault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BWVault.swift; sourceTree = ""; }; 1D6A491F29CF7A490011DF74 /* NSPopoverExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPopoverExtension.swift; sourceTree = ""; }; + 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DockCustomizerMock.swift; sourceTree = ""; }; 1D77921728FDC54C00BE0210 /* FaviconReferenceCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconReferenceCacheTests.swift; sourceTree = ""; }; 1D77921928FDC79800BE0210 /* FaviconStoringMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconStoringMock.swift; sourceTree = ""; }; 1D77921C28FFF27C00BE0210 /* RunningApplicationCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunningApplicationCheck.swift; sourceTree = ""; }; @@ -2842,6 +2852,7 @@ 1D8C2FE92B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWebViewSnapshotRenderer.swift; sourceTree = ""; }; 1D8C2FEC2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockViewSnapshotRenderer.swift; sourceTree = ""; }; 1D8C2FEF2B70F751005E4BBD /* MockTabSnapshotStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockTabSnapshotStore.swift; sourceTree = ""; }; + 1D9A37662BD8EA8800EBC58D /* DockPositionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DockPositionProvider.swift; sourceTree = ""; }; 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSnapshotExtension.swift; sourceTree = ""; }; 1D9FDEB62B9B5D150040B78C /* SearchPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPreferencesTests.swift; sourceTree = ""; }; 1D9FDEB92B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebTrackingProtectionPreferencesTests.swift; sourceTree = ""; }; @@ -2851,6 +2862,7 @@ 1D9FDEC52B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyProtectionStatusTests.swift; sourceTree = ""; }; 1DA6D0FC2A1FF9A100540406 /* HTTPCookie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPCookie.swift; sourceTree = ""; }; 1DA6D0FF2A1FF9DC00540406 /* HTTPCookieTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPCookieTests.swift; sourceTree = ""; }; + 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DockPositionProviderTests.swift; sourceTree = ""; }; 1DB67F282B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewSnapshotRenderer.swift; sourceTree = ""; }; 1DB67F2C2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewSnapshotRenderer.swift; sourceTree = ""; }; 1DB9617929F1D06D00CF5568 /* InternalUserDeciderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalUserDeciderMock.swift; sourceTree = ""; }; @@ -6514,6 +6526,8 @@ 858A798226A8B75F00A75A42 /* CopyHandler.swift */, 1D36E65A298ACD2900AA485D /* AppIconChanger.swift */, CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */, + 1D4071AD2BD64266002D4537 /* DockCustomizer.swift */, + 1D9A37662BD8EA8800EBC58D /* DockPositionProvider.swift */, 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */, ); path = Application; @@ -7928,6 +7942,8 @@ B6A5A2A725BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift */, B6A5A29F25B96E8300AA7ADA /* AppStateChangePublisherTests.swift */, B6C2C9EE276081AB005B7F0A /* DeallocationTests.swift */, + 1D7693FE2BE3A1AA0016A22B /* DockCustomizerMock.swift */, + 1DA860712BE3AE950027B813 /* DockPositionProviderTests.swift */, ); path = App; sourceTree = ""; @@ -9821,6 +9837,7 @@ B603971B29BA084C00902A34 /* JSAlertController.swift in Sources */, 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */, + 1D4071AF2BD64267002D4537 /* DockCustomizer.swift in Sources */, 4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, 1E559BB22BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */, 7B7FCD102BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */, @@ -10185,6 +10202,7 @@ 9F872D992B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, B6B4D1D02B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */, 3706FC57293F65D500E42796 /* TabPreviewWindowController.swift in Sources */, + 1D9A37682BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */, 3706FC58293F65D500E42796 /* NSSizeExtension.swift in Sources */, 3706FC59293F65D500E42796 /* Fire.swift in Sources */, 3706FC5A293F65D500E42796 /* RandomAccessCollectionExtension.swift in Sources */, @@ -10385,6 +10403,7 @@ 56B234C02A84EFD800F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */, 3706FE19293F661700E42796 /* DeviceAuthenticatorTests.swift in Sources */, 3706FE1A293F661700E42796 /* BrowserProfileTests.swift in Sources */, + 1D7694002BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */, 3706FE1B293F661700E42796 /* PermissionManagerTests.swift in Sources */, 3706FE1C293F661700E42796 /* ConnectBitwardenViewModelTests.swift in Sources */, 4B9DB0552A983B55000927DB /* MockWaitlistStorage.swift in Sources */, @@ -10520,6 +10539,7 @@ 9F872D9E2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */, 3706FE68293F661700E42796 /* DuckPlayerURLExtensionTests.swift in Sources */, 3706FE6A293F661700E42796 /* FirefoxKeyReaderTests.swift in Sources */, + 1DA860732BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */, 3706FE6B293F661700E42796 /* AppKitPrivateMethodsAvailabilityTests.swift in Sources */, 3706FE6D293F661700E42796 /* ChromiumBookmarksReaderTests.swift in Sources */, C1E961F42B87B276001760E1 /* MockAutofillActionPresenter.swift in Sources */, @@ -11333,6 +11353,7 @@ 1D69C553291302F200B75945 /* BWVault.swift in Sources */, AA6FFB4424DC33320028F4D0 /* NSViewExtension.swift in Sources */, B6C0B23E26E8BF1F0031CB7F /* DownloadListViewModel.swift in Sources */, + 1D9A37672BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */, 4B9292D52667123700AD2C21 /* BookmarkManagementDetailViewController.swift in Sources */, F188267C2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */, 4B723E1026B0006700E14D75 /* CSVImporter.swift in Sources */, @@ -11550,6 +11571,7 @@ B6AAAC3E26048F690029438D /* RandomAccessCollectionExtension.swift in Sources */, 4B9292AF26670F5300AD2C21 /* NSOutlineViewExtensions.swift in Sources */, 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, + 1D4071AE2BD64267002D4537 /* DockCustomizer.swift in Sources */, AA585D82248FD31100E9A3E2 /* AppDelegate.swift in Sources */, 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */, C1DAF3B52B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */, @@ -11742,6 +11764,7 @@ 4B11060525903E570039B979 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, 858A798826A99DBE00A75A42 /* PasswordManagementItemListModelTests.swift in Sources */, 566B196529CDB828007E38F4 /* CapturingOptionsButtonMenuDelegate.swift in Sources */, + 1D7693FF2BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */, 4B8AD0B127A86D9200AE44D6 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, 9FA5A0B02BC9039200153786 /* BookmarkFolderStoreMock.swift in Sources */, B69B50472726C5C200758A2B /* VariantManagerTests.swift in Sources */, @@ -11866,6 +11889,7 @@ B6A5A2A825BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift in Sources */, B6AE39F129373AF200C37AA4 /* EmptyAttributionRulesProver.swift in Sources */, 4BB99D1126FE1A84001E4761 /* SafariBookmarksReaderTests.swift in Sources */, + 1DA860722BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */, 4BBF0925283083EC00EE1418 /* FileSystemDSLTests.swift in Sources */, 4B11060A25903EAC0039B979 /* CoreDataEncryptionTests.swift in Sources */, B603971029B9D67E00902A34 /* PublishersExtensions.swift in Sources */, diff --git a/DuckDuckGo/Application/DockCustomizer.swift b/DuckDuckGo/Application/DockCustomizer.swift new file mode 100644 index 0000000000..6a889bd058 --- /dev/null +++ b/DuckDuckGo/Application/DockCustomizer.swift @@ -0,0 +1,146 @@ +// +// DockCustomizer.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Common + +protocol DockCustomization { + var isAddedToDock: Bool { get } + + @discardableResult + func addToDock() -> Bool +} + +final class DockCustomizer: DockCustomization { + + private let positionProvider: DockPositionProviding + + init(positionProvider: DockPositionProviding = DockPositionProvider()) { + self.positionProvider = positionProvider + } + + private var dockPlistURL: URL = URL(fileURLWithPath: NSString(string: "~/Library/Preferences/com.apple.dock.plist").expandingTildeInPath) + + private var dockPlistDict: [String: AnyObject]? { + return NSDictionary(contentsOf: dockPlistURL) as? [String: AnyObject] + } + + // This checks whether the bundle identifier of the current bundle + // is present in the 'persistent-apps' array of the Dock's plist. + var isAddedToDock: Bool { + guard let bundleIdentifier = Bundle.main.bundleIdentifier, + let dockPlistDict = dockPlistDict, + let persistentApps = dockPlistDict["persistent-apps"] as? [[String: AnyObject]] else { + return false + } + + return persistentApps.contains(where: { ($0["tile-data"] as? [String: AnyObject])?["bundle-identifier"] as? String == bundleIdentifier }) + } + + // Adds a dictionary representing the application, either by using an existing + // one from 'recent-apps' or creating a new one if the application isn't recently used. + // It then inserts this dictionary into the 'persistent-apps' list at a position + // determined by `positionProvider`. Following the plist update, it schedules the Dock + // to restart after a brief delay to apply the changes. + @discardableResult + func addToDock() -> Bool { + let appPath = Bundle.main.bundleURL.path + guard !isAddedToDock, + let bundleIdentifier = Bundle.main.bundleIdentifier, + var dockPlistDict = dockPlistDict else { + return false + } + + var persistentApps = dockPlistDict["persistent-apps"] as? [[String: AnyObject]] ?? [] + let recentApps = dockPlistDict["recent-apps"] as? [[String: AnyObject]] ?? [] + + let appDict: [String: AnyObject] + // Find the app in recent apps + if let recentAppIndex = recentApps.firstIndex(where: { appDict in + if let tileData = appDict["tile-data"] as? [String: AnyObject], + let appBundleIdentifier = tileData["bundle-identifier"] as? String { + return appBundleIdentifier == bundleIdentifier + } + return false + }) { + // Use existing dictonary from recentApps + appDict = recentApps[recentAppIndex] + } else { + // Create the dictionary for the current application if not found in recent apps + appDict = Self.appDict(appPath: appPath, bundleIdentifier: bundleIdentifier) + } + + // Insert to persistent apps + let index = positionProvider.newDockIndex(from: makeAppURLs(from: persistentApps)) + persistentApps.insert(appDict, at: index) + + // Update the plist + dockPlistDict["persistent-apps"] = persistentApps as AnyObject? + dockPlistDict["recent-apps"] = recentApps as AnyObject? + + // Update mod-count + dockPlistDict["mod-count"] = ((dockPlistDict["mod-count"] as? Int) ?? 0) + 1 as AnyObject + + do { + try (dockPlistDict as NSDictionary).write(to: dockPlistURL) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.restartDock() + } + return true + } catch { + os_log(.error, "Error writing to Dock plist: %{public}@", error.localizedDescription) + return false + } + } + + private func restartDock() { + let task = Process() + task.launchPath = "/usr/bin/killall" + task.arguments = ["Dock"] + task.launch() + } + + private func makeAppURLs(from persistentApps: [[String: AnyObject]]) -> [URL] { + return persistentApps.compactMap { appDict in + if let tileData = appDict["tile-data"] as? [String: AnyObject], + let appBundleIdentifier = tileData["file-data"] as? [String: AnyObject], + let urlString = appBundleIdentifier["_CFURLString"] as? String, + let url = URL(string: urlString) { + return url + } else { + return nil + } + } + } + + static func appDict(appPath: String, bundleIdentifier: String) -> [String: AnyObject] { + return ["tile-type": "file-tile" as AnyObject, + "tile-data": [ + "dock-extra": 0 as AnyObject, + "file-type": 1 as AnyObject, + "file-data": [ + "_CFURLString": "file://" + appPath + "/", + "_CFURLStringType": 15 + ], + "file-label": "DuckDuckGo" as AnyObject, + "bundle-identifier": bundleIdentifier as AnyObject, + "is-beta": 0 as AnyObject + ] as AnyObject + ] + } +} diff --git a/DuckDuckGo/Application/DockPositionProvider.swift b/DuckDuckGo/Application/DockPositionProvider.swift new file mode 100644 index 0000000000..f93ba428c1 --- /dev/null +++ b/DuckDuckGo/Application/DockPositionProvider.swift @@ -0,0 +1,79 @@ +// +// DockPositionProvider.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 + +enum DockApp: String, CaseIterable { + case chrome = "/Applications/Google Chrome.app/" + case firefox = "/Applications/Firefox.app/" + case edge = "/Applications/Microsoft Edge.app/" + case brave = "/Applications/Brave Browser.app/" + case opera = "/Applications/Opera.app/" + case arc = "/Applications/Arc.app/" + case safari = "/Applications/Safari.app/" + case safariLong = "/System/Volumes/Preboot/Cryptexes/App/System/Applications/Safari.app/" + + var url: URL { + return URL(string: "file://" + self.rawValue)! + } +} + +protocol DockPositionProviding { + func newDockIndex(from currentAppURLs: [URL]) -> Int +} + +/// Class to determine the best positioning in the Dock +final class DockPositionProvider: DockPositionProviding { + + private let preferredOrder: [DockApp] = [ + .chrome, + .firefox, + .edge, + .brave, + .opera, + .arc, + .safari, + .safariLong + ] + + private var defaultBrowserProvider: DefaultBrowserProvider + + init(defaultBrowserProvider: DefaultBrowserProvider = SystemDefaultBrowserProvider()) { + self.defaultBrowserProvider = defaultBrowserProvider + } + + /// Determines the new dock index for a new app based on the default browser or preferred order + func newDockIndex(from currentAppURLs: [URL]) -> Int { + // Place next to the default browser + if !defaultBrowserProvider.isDefault, + let defaultBrowserURL = defaultBrowserProvider.defaultBrowserURL, + let position = currentAppURLs.firstIndex(of: defaultBrowserURL) { + return position + 1 + } + + // Place based on the preferred order + for app in preferredOrder { + if let position = currentAppURLs.firstIndex(of: app.url) { + return position + 1 + } + } + + // Otherwise, place at the end + return currentAppURLs.count + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json new file mode 100644 index 0000000000..e7bb0888d9 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Dock-128.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Dock-128.pdf b/DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Dock-128.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9a0511ce2c0c14e8f62484a2e049937b18473ab2 GIT binary patch literal 12816 zcmeHNTW=e=6@H&zF)smj3q%@{LkmRNiMQo?0@ff zNX|#<;z~|>LEFrOO?^Wio;wdYe0KTz&2u&Cs!G*Oy?Xy+RoCBrSC7A+EpN9UYYp%U z5T5bXWO+NQ|5!JpCOowo8s{K?N>pC2?k4l)Q8Gi9HxJ9{cD`Ev{-^s{-D2E}7u9CF zo=xtmpKDCt8Pix-&pu5*%CEk4t{Z;!4o~`Z^KhS;z1`M7$4>XF`y-8=&YOq#_48(A zOsBQ3?;aL;*2&FH9DQtnHr^RO42X<9J^*Sn@}&NWb@uGa zoma`4+Kbx;Ws}a?%7&lmF#NRoL?=;ws_m&hOH|bN^5yDN83Q?jv>&3XcSCE?-_>lh zdRR|qn-HRxlkIl4UdFZx|K3fu>$wnqu5}!&YpX{##_RK)BuD)7jhg zyp@+=w4>`beSa!Qco@Wh&`^DLAqvqK2<1__8QD#}NU4bmq<0@GvqTT~yb!7Xc; zXMCyZ&2(9PsFu}{1yX!1X86{2%`b{?|GzbYRe$-q}{U+#z2(G-$Xu2h=m zrQ9!1vV%fGpq`gj$Dfx-hn#Rk_`w-YFmX< zeo2>9(PQi$Co;a!Sw*4lYq$vQGBUofXZP%L&l75IbI%jAo3neWCU-qCeJmz}Cp|uW z&>a2@`vbp|{?e-4i}epzMdXZPQIuaux__z07plJ9RM?NEr7$mwxy6@CnW3MJEv$86 zeY9~~)r|HVlGD2p3f#6ebvp_h0#)BtcI36Uov-ak_43pUV7ArXwZOE+2&|)_felrB zWo_#29U4Nw_4TxhWK~{CHGMhwij_L+%nXUS zlQuJmEFt-#6>7oJf`%g}c~^myfR6b zybV?g;)$;FtaOXrfHqGEXeJO7TW7`dVMh77BL6?H$j!fbMV6}Ie|bf2C4gBrGr=7z zskI%FWE;zZvjP}P*j^bV2U4yRG^broeE5n4iiJgwj7|g+C6@c_PDDz?)SVa`hp$LL z7NoQ)kSlg4!|YCEH$&R96`T+9bWGGz-iosJcPd0=%fdX)`DIAIr*pnMd_@9s26)>dYq-uz~c? zvWai8BlD4nRG(?A<6N8|LwtvFl5IAm)=pH|IF6OODKAPqlBtiKkvNa-Zf{%g>qdR= zEcPv@)u^Phb~yVG6&QR##%4;+FtqHKh{^u6b*4eVNGvtvv_M|<&G1S?VSzx0m6g3v z`?lJECsIOv+~@{I;(SCMJ|eqGsDFYb zWH)P^Hz?XI{7-w>_=>r#? z)fN*`B02BDp^Xch!z4GhTu`kwSvO{6`@Yd~qLnl#3)j9@U*guj*jl{Yh%y@0V-?w1c=86jSb3D} z=y#VM#c4Q8hRS_C%b-!(?lGnHoDGt#X{18yK7Vrva(3B zLc_LLZQZWr+;A9OYzpRNr~*_s+BlZ5onAFvW1;2zUe8ivB@HsvmlCAuW-@0&Jbz9aAYDADO!BXA$fq* zz6Yc;1BX13Mxj5FV1pFcorvt1I&3|$`Gh#3fm{Ol!(}puii#jU-iLltC(1oFB(4la z8lm3JU?PIDupk$ypl6_dh7lBSAzL;lvzTDTJM^ax+_)HpDT zk2RQ7o&ti4>_Of_^xa-wLVX8<*FuwfkbIEbfv_qknE>-d*L`{A8KXG zoO*57llfw{u0(aed5xRD-)vTQvv2Ef<~QF)ON_5pE0{sF9Lznwp8sto8iNfs+sS$x Qbf|ROx$4=oiyz+n9rjo5;s5{u literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 53319c1e7b..02fb18ac46 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -616,6 +616,10 @@ struct UserText { static let isDefaultBrowser = NSLocalizedString("preferences.default-browser.active", value: "DuckDuckGo is your default browser", comment: "Indicate that the browser is the default") static let isNotDefaultBrowser = NSLocalizedString("preferences.default-browser.inactive", value: "DuckDuckGo is not your default browser.", comment: "Indicate that the browser is not the default") static let makeDefaultBrowser = NSLocalizedString("preferences.default-browser.button.make-default", value: "Make DuckDuckGo Default…", comment: "represents a prompt message asking the user to make DuckDuckGo their default browser.") + static let shortcuts = NSLocalizedString("preferences.shortcuts", value: "Shortcuts", comment: "Name of the preferences section related to shortcuts") + static let isAddedToDock = NSLocalizedString("preferences.is-added-to-dock", value: "DuckDuckGo is added to the Dock.", comment: "Indicates that the browser is added to the macOS system Dock") + static let isNotAddedToDock = NSLocalizedString("preferences.not-added-to-dock", value: "DuckDuckGo is not added to the Dock.", comment: "Indicate that the browser is not added to macOS system Dock") + static let addToDock = NSLocalizedString("preferences.add-to-dock", value: "Add to Dock…", comment: "Action button to add the app to the Dock") static let onStartup = NSLocalizedString("preferences.on-startup", value: "On Startup", comment: "Name of the preferences section related to app startup") static let reopenAllWindowsFromLastSession = NSLocalizedString("preferences.reopen-windows", value: "Reopen all windows from last session", comment: "Option to control session restoration") static let showHomePage = NSLocalizedString("preferences.show-home", value: "Open a new window", comment: "Option to control session startup") @@ -794,11 +798,14 @@ struct UserText { static let onboardingWelcomeText = NSLocalizedString("onboarding.welcome.text", value: "Tired of being tracked online? You've come to the right place 👍\n\nI'll help you stay private️ as you search and browse the web. Trackers be gone!", comment: "Detailed welcome to the app text") static let onboardingImportDataText = NSLocalizedString("onboarding.importdata.text", value: "First, let me help you import your bookmarks 📖 and passwords 🔑 from those less private browsers.", comment: "Call to action to import data from other browsers") static let onboardingSetDefaultText = NSLocalizedString("onboarding.setdefault.text", value: "Next, try setting DuckDuckGo as your default️ browser, so you can open links with peace of mind, every time.", comment: "Call to action to set the browser as default") + static let onboardingAddToDockText = NSLocalizedString("onboarding.addtodock.text", value: "One last thing. Want to keep DuckDuckGo in your Dock so the browser's always within reach?", comment: "Call to action to add the DuckDuckGo app icon to the macOS system dock") static let onboardingStartBrowsingText = NSLocalizedString("onboarding.startbrowsing.text", value: "You’re all set!\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible\u{00A0}🔒", comment: "Call to action to start using the app as a browser") + static let onboardingStartBrowsingAddedToDockText = NSLocalizedString("onboarding.startbrowsing.added-to-dock.text", value: "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible\u{00A0}🔒", comment: "Call to action to start using the app as a browser") static let onboardingStartButton = NSLocalizedString("onboarding.welcome.button", value: "Get Started", comment: "Start the onboarding flow") static let onboardingImportDataButton = NSLocalizedString("onboarding.importdata.button", value: "Import", comment: "Launch the import data UI") static let onboardingSetDefaultButton = NSLocalizedString("onboarding.setdefault.button", value: "Let's Do It!", comment: "Launch the set default UI") + static let onboardingAddToDockButton = NSLocalizedString("onboarding.addtodock.button", value: "Keep in Dock", comment: "Button label to add application to the macOS system dock") static let onboardingNotNowButton = NSLocalizedString("onboarding.notnow.button", value: "Maybe Later", comment: "Skip a step of the onboarding flow") static func importingBookmarks(_ numberOfBookmarks: Int?) -> String { @@ -1070,17 +1077,21 @@ struct UserText { // Set Up static let newTabSetUpSectionTitle = NSLocalizedString("newTab.setup.section.title", value: "Next Steps", comment: "Title of the setup section in the home page") static let newTabSetUpDefaultBrowserCardTitle = NSLocalizedString("newTab.setup.default.browser.title", value: "Default to Privacy", comment: "Title of the Default Browser card of the Set Up section in the home page") + static let newTabSetUpDockCardTitle = NSLocalizedString("newTab.setup.dock.title", value: "Keep in Your Dock", comment: "Title of the new tab page card for adding application to the Dock") static let newTabSetUpImportCardTitle = NSLocalizedString("newTab.setup.import.title", value: "Bring Your Stuff", comment: "Title of the Import card of the Set Up section in the home page") static let newTabSetUpDuckPlayerCardTitle = NSLocalizedString("newTab.setup.duck.player.title", value: "Clean Up YouTube", comment: "Title of the Duck Player card of the Set Up section in the home page") static let newTabSetUpEmailProtectionCardTitle = NSLocalizedString("newTab.setup.email.protection.title", value: "Protect Your Inbox", comment: "Title of the Email Protection card of the Set Up section in the home page") static let newTabSetUpDefaultBrowserAction = NSLocalizedString("newTab.setup.default.browser.action", value: "Make Default Browser", comment: "Action title on the action menu of the Default Browser card") + static let newTabSetUpDockAction = NSLocalizedString("newTab.setup.dock.action", value: "Keep In Dock", comment: "Action title on the action menu of the 'Add App to the Dock' card") + static let newTabSetUpDockConfirmation = NSLocalizedString("newTab.setup.dock.confirmation", value: "Added to Dock!", comment: "Confirmation title after user clicks on 'Add to Dock' card") static let newTabSetUpImportAction = NSLocalizedString("newTab.setup.Import.action", value: "Import Now", comment: "Action title on the action menu of the Import card of the Set Up section in the home page") static let newTabSetUpDuckPlayerAction = NSLocalizedString("newTab.setup.duck.player.action", value: "Try Duck Player", comment: "Action title on the action menu of the Duck Player card of the Set Up section in the home page") static let newTabSetUpEmailProtectionAction = NSLocalizedString("newTab.setup.email.protection.action", value: "Get a Duck Address", comment: "Action title on the action menu of the Email Protection card of the Set Up section in the home page") static let newTabSetUpRemoveItemAction = NSLocalizedString("newTab.setup.remove.item", value: "Dismiss", comment: "Action title on the action menu of the set up cards card of the SetUp section in the home page to remove the item") static let newTabSetUpDefaultBrowserSummary = NSLocalizedString("newTab.setup.default.browser.summary", value: "We automatically block trackers as you browse. It's privacy, simplified.", comment: "Summary of the Default Browser card") + static let newTabSetUpDockSummary = NSLocalizedString("newTab.setup.dock.summary", value: "Get to DuckDuckGo faster by adding it to your Dock.", comment: "Summary of the 'Add App to the Dock' card") static let newTabSetUpImportSummary = NSLocalizedString("newTab.setup.import.summary", value: "Import bookmarks, favorites, and passwords from your old browser.", comment: "Summary of the Import card of the Set Up section in the home page") static let newTabSetUpDuckPlayerSummary = NSLocalizedString("newTab.setup.duck.player.summary", value: "Enjoy a clean viewing experience without personalized ads.", comment: "Summary of the Duck Player card of the Set Up section in the home page") static let newTabSetUpEmailProtectionSummary = NSLocalizedString("newTab.setup.email.protection.summary", value: "Generate custom @duck.com addresses that clean trackers from incoming email.", comment: "Summary of the Email Protection card of the Set Up section in the home page") diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 5c72907b8e..13193f793f 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -117,6 +117,7 @@ public struct UserDefaultsWrapper { case homePageShowAllFavorites = "home.page.show.all.favorites" case homePageShowAllFeatures = "home.page.show.all.features" case homePageShowMakeDefault = "home.page.show.make.default" + case homePageShowAddToDock = "home.page.show.add.to.dock" case homePageShowImport = "home.page.show.import" case homePageShowDuckPlayer = "home.page.show.duck.player" case homePageShowEmailProtection = "home.page.show.email.protection" diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index b99a4a2949..22ddf43e7e 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -48,6 +48,7 @@ extension HomePage.Models { } private let defaultBrowserProvider: DefaultBrowserProvider + private let dockCustomizer: DockCustomization private let dataImportProvider: DataImportStatusProviding private let tabCollectionViewModel: TabCollectionViewModel private let emailManager: EmailManager @@ -63,6 +64,9 @@ extension HomePage.Models { @UserDefaultsWrapper(key: .homePageShowMakeDefault, defaultValue: true) private var shouldShowMakeDefaultSetting: Bool + @UserDefaultsWrapper(key: .homePageShowAddToDock, defaultValue: true) + private var shouldShowAddToDockSetting: Bool + @UserDefaultsWrapper(key: .homePageShowImport, defaultValue: true) private var shouldShowImportSetting: Bool @@ -100,6 +104,7 @@ extension HomePage.Models { @Published var visibleFeaturesMatrix: [[FeatureType]] = [[]] init(defaultBrowserProvider: DefaultBrowserProvider, + dockCustomizer: DockCustomization, dataImportProvider: DataImportStatusProviding, tabCollectionViewModel: TabCollectionViewModel, emailManager: EmailManager = EmailManager(), @@ -108,6 +113,7 @@ extension HomePage.Models { privacyConfigurationManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, permanentSurveyManager: SurveyManager = PermanentSurveyManager()) { self.defaultBrowserProvider = defaultBrowserProvider + self.dockCustomizer = dockCustomizer self.dataImportProvider = dataImportProvider self.tabCollectionViewModel = tabCollectionViewModel self.emailManager = emailManager @@ -125,22 +131,15 @@ extension HomePage.Models { @MainActor func performAction(for featureType: FeatureType) { switch featureType { case .defaultBrowser: - do { - PixelKit.fire(GeneralPixel.defaultRequestedFromHomepageSetupView) - try defaultBrowserProvider.presentDefaultBrowserPrompt() - } catch { - defaultBrowserProvider.openSystemPreferences() - } + performDefaultBrowserAction() + case .dock: + performDockAction() case .importBookmarksAndPasswords: - dataImportProvider.showImportWindow(completion: {self.refreshFeaturesMatrix()}) + performImportBookmarksAndPasswordsAction() case .duckplayer: - if let videoUrl = URL(string: duckPlayerURL) { - let tab = Tab(content: .url(videoUrl, source: .link), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) - } + performDuckPlayerAction() case .emailProtection: - let tab = Tab(content: .url(EmailUrls().emailProtectionLink, source: .ui), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) + performEmailProtectionAction() case .permanentSurvey: visitSurvey() case .networkProtectionRemoteMessage(let message): @@ -148,16 +147,56 @@ extension HomePage.Models { case .dataBrokerProtectionRemoteMessage(let message): handle(remoteMessage: message) case .dataBrokerProtectionWaitlistInvited: -#if DBP - DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .cardUI) -#endif + performDataBrokerProtectionWaitlistInvitedAction() + } + } + + private func performDefaultBrowserAction() { + do { + PixelKit.fire(GeneralPixel.defaultRequestedFromHomepageSetupView) + try defaultBrowserProvider.presentDefaultBrowserPrompt() + } catch { + defaultBrowserProvider.openSystemPreferences() } } + private func performImportBookmarksAndPasswordsAction() { + dataImportProvider.showImportWindow(completion: { self.refreshFeaturesMatrix() }) + } + + @MainActor + private func performDuckPlayerAction() { + if let videoUrl = URL(string: duckPlayerURL) { + let tab = Tab(content: .url(videoUrl, source: .link), shouldLoadInBackground: true) + tabCollectionViewModel.append(tab: tab) + } + } + + @MainActor + private func performEmailProtectionAction() { + let tab = Tab(content: .url(EmailUrls().emailProtectionLink, source: .ui), shouldLoadInBackground: true) + tabCollectionViewModel.append(tab: tab) + } + + @MainActor + private func performDataBrokerProtectionWaitlistInvitedAction() { + #if DBP + DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .cardUI) + #endif + } + + func performDockAction() { + PixelKit.fire(GeneralPixel.userAddedToDockFromNewTabPageCard, + includeAppVersionParameter: false) + dockCustomizer.addToDock() + } + func removeItem(for featureType: FeatureType) { switch featureType { case .defaultBrowser: shouldShowMakeDefaultSetting = false + case .dock: + shouldShowAddToDockSetting = false case .importBookmarksAndPasswords: shouldShowImportSetting = false case .duckplayer: @@ -196,7 +235,6 @@ extension HomePage.Models { for message in homePageRemoteMessaging.networkProtectionRemoteMessaging.presentableRemoteMessages() { PixelKit.fire(GeneralPixel.networkProtectionRemoteMessageDisplayed(messageID: message.id), frequency: .daily) } - appendFeatureCards(&features) featuresMatrix = features.chunked(into: itemsPerRow) @@ -214,6 +252,8 @@ extension HomePage.Models { return shouldMakeDefaultCardBeVisible case .importBookmarksAndPasswords: return shouldImportCardBeVisible + case .dock: + return shouldDockCardBeVisible case .duckplayer: return shouldDuckPlayerCardBeVisible case .emailProtection: @@ -274,6 +314,15 @@ extension HomePage.Models { !defaultBrowserProvider.isDefault } + private var shouldDockCardBeVisible: Bool { +#if !APPSTORE + shouldShowAddToDockSetting && + !dockCustomizer.isAddedToDock +#else + return false +#endif + } + private var shouldImportCardBeVisible: Bool { shouldShowImportSetting && !dataImportProvider.didImport @@ -369,12 +418,17 @@ extension HomePage.Models { // We ignore the `networkProtectionRemoteMessage` case here to avoid it getting accidentally included - it has special handling and will get // included elsewhere. static var allCases: [HomePage.Models.FeatureType] { +#if APPSTORE [.duckplayer, .emailProtection, .defaultBrowser, .importBookmarksAndPasswords, .permanentSurvey] +#else + [.duckplayer, .emailProtection, .defaultBrowser, .dock, .importBookmarksAndPasswords, .permanentSurvey] +#endif } case duckplayer case emailProtection case defaultBrowser + case dock case importBookmarksAndPasswords case permanentSurvey case networkProtectionRemoteMessage(NetworkProtectionRemoteMessage) @@ -385,6 +439,8 @@ extension HomePage.Models { switch self { case .defaultBrowser: return UserText.newTabSetUpDefaultBrowserCardTitle + case .dock: + return UserText.newTabSetUpDockCardTitle case .importBookmarksAndPasswords: return UserText.newTabSetUpImportCardTitle case .duckplayer: @@ -406,6 +462,8 @@ extension HomePage.Models { switch self { case .defaultBrowser: return UserText.newTabSetUpDefaultBrowserSummary + case .dock: + return UserText.newTabSetUpDockSummary case .importBookmarksAndPasswords: return UserText.newTabSetUpImportSummary case .duckplayer: @@ -427,6 +485,8 @@ extension HomePage.Models { switch self { case .defaultBrowser: return UserText.newTabSetUpDefaultBrowserAction + case .dock: + return UserText.newTabSetUpDockAction case .importBookmarksAndPasswords: return UserText.newTabSetUpImportAction case .duckplayer: @@ -444,12 +504,23 @@ extension HomePage.Models { } } + var confirmation: String? { + switch self { + case .dock: + return UserText.newTabSetUpDockConfirmation + default: + return nil + } + } + var icon: NSImage { let iconSize = NSSize(width: 64, height: 48) switch self { case .defaultBrowser: return .defaultApp128.resized(to: iconSize)! + case .dock: + return .dock128.resized(to: iconSize)! case .importBookmarksAndPasswords: return .import128.resized(to: iconSize)! case .duckplayer: diff --git a/DuckDuckGo/HomePage/View/ContinueSetUpView.swift b/DuckDuckGo/HomePage/View/ContinueSetUpView.swift index 1fa9e6639b..e6d56fd18f 100644 --- a/DuckDuckGo/HomePage/View/ContinueSetUpView.swift +++ b/DuckDuckGo/HomePage/View/ContinueSetUpView.swift @@ -18,6 +18,7 @@ import SwiftUI import SwiftUIExtensions +import PixelKit extension HomePage.Views { @@ -100,7 +101,7 @@ extension HomePage.Views { .frame(width: 24, height: 24) } ZStack { - CardTemplate(title: featureType.title, summary: featureType.summary, actionText: featureType.action, icon: icon, width: model.itemWidth, height: model.itemHeight, action: { model.performAction(for: featureType) }) + CardTemplate(title: featureType.title, summary: featureType.summary, actionText: featureType.action, confirmationText: featureType.confirmation, icon: icon, width: model.itemWidth, height: model.itemHeight, action: { model.performAction(for: featureType) }) .contextMenu(ContextMenu(menuItems: { Button(featureType.action, action: { model.performAction(for: featureType) }) Divider() @@ -121,6 +122,13 @@ extension HomePage.Views { .onHover { isHovering in self.isHovering = isHovering } + .onAppear { + if featureType == .dock { + PixelKit.fire(GeneralPixel.addToDockNewTabPageCardPresented, + frequency: .unique, + includeAppVersionParameter: false) + } + } } } @@ -129,12 +137,14 @@ extension HomePage.Views { var title: String var summary: String var actionText: String + var confirmationText: String? @ViewBuilder var icon: Content let width: CGFloat let height: CGFloat let action: () -> Void @State var isHovering = false + @State var isClicked = false var body: some View { ZStack(alignment: .center) { @@ -166,7 +176,23 @@ extension HomePage.Views { .frame(width: 208, height: 130) VStack { Spacer() - ActionButton(title: actionText, isHoveringOnCard: $isHovering, action: action) + if let confirmationText, isClicked { + HStack { + Image(.successCheckmark) + Text(confirmationText) + .bold() + .multilineTextAlignment(.center) + .lineLimit(1) + .font(.system(size: 11)) + .fixedSize(horizontal: false, vertical: true) + } + .offset(y: -3) + } else { + ActionButton(title: actionText, + isHoveringOnCard: $isHovering, + isClicked: $isClicked, + action: action) + } } .padding(8) } @@ -188,11 +214,13 @@ extension HomePage.Views { @State var isHovering = false @Binding var isHoveringOnCard: Bool + @Binding var isClicked: Bool - init(title: String, isHoveringOnCard: Binding, action: @escaping () -> Void) { + init(title: String, isHoveringOnCard: Binding, isClicked: Binding, action: @escaping () -> Void) { self.title = title self.action = action self._isHoveringOnCard = isHoveringOnCard + self._isClicked = isClicked self.titleWidth = (title as NSString).size(withAttributes: [.font: NSFont.systemFont(ofSize: 11) as Any]).width + 14 } @@ -217,6 +245,7 @@ extension HomePage.Views { .foregroundColor(Color(.linkBlue)) } .onTapGesture { + isClicked = true action() } .onHover { isHovering in diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index 9769333182..092a33384c 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -150,6 +150,7 @@ final class HomePageViewController: NSViewController { func createFeatureModel() -> HomePage.Models.ContinueSetUpModel { return HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: SystemDefaultBrowserProvider(), + dockCustomizer: DockCustomizer(), dataImportProvider: BookmarksAndPasswordsImportStatusProvider(), tabCollectionViewModel: tabCollectionViewModel, duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor(), diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 9ff3c8becb..4503c03e71 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -30609,6 +30609,246 @@ } } }, + "newTab.setup.dock.action" : { + "comment" : "Action title on the action menu of the 'Add App to the Dock' card", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Im Dock behalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep In Dock" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantener en Dock" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Garder dans le Dock" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resta nel dock" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "In het Dock houden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trzymaj w Docku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manter na Dock" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык для док-панели" + } + } + } + }, + "newTab.setup.dock.confirmation" : { + "comment" : "Confirmation title after user clicks on 'Add to Dock' card", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zum Dock hinzugefügt!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Added to Dock!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Añadido al Dock!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouté au Dock !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiunto al dock!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toegevoegd aan Dock!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodano do Docka!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionado à Dock!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык добавлен на док-панель." + } + } + } + }, + "newTab.setup.dock.summary" : { + "comment" : "Summary of the 'Add App to the Dock' card", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kannst DuckDuckGo schneller erreichen, indem du es zu deinem Dock hinzufügst." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Get to DuckDuckGo faster by adding it to your Dock." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accede a DuckDuckGo más rápido añadiéndolo a tu Dock." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accédez plus rapidement à DuckDuckGo en l'ajoutant à votre Dock." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accedi più velocemente a DuckDuckGo aggiungendolo al tuo dock." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ga sneller naar DuckDuckGo door het aan je Dock toe te voegen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzyskuj szybszy dostęp do przeglądarki DuckDuckGo dzięki jej dodaniu do Docka." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acede ao DuckDuckGo mais rapidamente adicionando-o à tua Dock." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавьте DuckDuckGo на док-панель для быстрого запуска." + } + } + } + }, + "newTab.setup.dock.title" : { + "comment" : "Title of the new tab page card for adding application to the Dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "In deinem Dock behalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep in Your Dock" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantener en tu Dock" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Garder dans votre Dock" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tieni nel tuo dock" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bewaar in je dock" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trzymaj w Docku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manter na tua Dock" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык для док-панели" + } + } + } + }, "newTab.setup.duck.player.action" : { "comment" : "Action title on the action menu of the Duck Player card of the Set Up section in the home page", "extractionState" : "extracted_with_value", @@ -31696,55 +31936,175 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "OK" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "OK" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "De acuerdo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ok" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хорошо" + } + } + } + }, + "onboarding.addtodock.button" : { + "comment" : "Button label to add application to the macOS system dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Im Dock behalten" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep in Dock" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantener en Dock" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Garder dans le Dock" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tieni nel Dock" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "In Dock bewaren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trzymaj w Docku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manter na Dock" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Держать на док-панели" + } + } + } + }, + "onboarding.addtodock.text" : { + "comment" : "Call to action to add the DuckDuckGo app icon to the macOS system dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eine letzte Sache. Du möchtest DuckDuckGo in deinem Dock haben, damit der Browser immer in Reichweite ist?" } }, "en" : { "stringUnit" : { "state" : "new", - "value" : "OK" + "value" : "One last thing. Want to keep DuckDuckGo in your Dock so the browser's always within reach?" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "De acuerdo" + "value" : "Una última cosa. ¿Quieres tener DuckDuckGo en tu Dock para que el navegador esté siempre al alcance de la mano?" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ok" + "value" : "Une dernière chose. Vous voulez garder DuckDuckGo dans votre Dock pour que le navigateur reste à portée de main ?" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Un'ultima cosa. Vuoi tenere DuckDuckGo nel tuo dock in modo che il browser sia sempre disponibile?" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Nog een laatste ding. Wil je DuckDuckGo in je Dock houden zodat de browser altijd binnen handbereik is?" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Ostatnia sprawa. Czy chcesz trzymać przeglądarkę DuckDuckGo w Docku, aby zawsze ją mieć pod ręką?" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Só mais uma coisa. Queres ter o navegador DuckDuckGo na tua Dock estar sempre à mão?" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Хорошо" + "value" : "И кое-что еще... Хотите сохранить DuckDuckGo на док-панели, чтобы наш браузер всегда был под рукой?" } } } @@ -32049,6 +32409,66 @@ } } }, + "onboarding.startbrowsing.added-to-dock.text" : { + "comment" : "Call to action to start using the app as a browser", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Du bist bereit! Du kannst mich jederzeit im Dock antreffen.\nMöchtest du sehen, wie ich dich beschütze? Versuche, eine deiner Lieblingsseiten zu besuchen 👆\n\nBehalte die Adressleiste im Auge. Ich werde Tracker blockieren und die Sicherheit deiner Verbindung verbessern, wenn möglichu{00A0}🔒" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒" + } + }, + "es" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "¡Ya está todo listo! Puedes encontrarme en el Dock en cualquier momento.\n¿Quieres ver cómo te protejo? Prueba a visitar uno de tus sitios favoritos 👆\n\nNo pierdas de vista la barra de direcciones al navegar. Bloquearé los rastreadores y mejoraré la seguridad de tu conexión cuando sea posible{00A0}🔒" + } + }, + "fr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Tout est prêt ! Vous pouvez me trouver sur le Dock à tout moment.\nVous voulez voir comment je vous protège ? Essayez de visiter l'un de vos sites préférés 👆\n\nContinuez à regarder la barre d'adresse au fur et à mesure. Je bloquerai les traqueurs et mettrai à niveau la sécurité de votre connexion si possible 🔒" + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Tutto pronto! Puoi trovarmi nel dock in qualsiasi momento.\nVuoi vedere come ti proteggo? Prova a visitare uno dei tuoi siti preferiti 👆\n\nContinua a controllare la barra degli indirizzi mentre esplori. Bloccherò i sistemi di tracciamento e aggiornerò la sicurezza della tua connessione quando possibile{00A0} 🔒" + } + }, + "nl" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Je bent helemaal klaar! Je kunt me altijd vinden in het Dock.\nWil je zien hoe ik je bescherm? Ga eens naar een van je favoriete websites 👆\n\nKijk tijdens het surfen goed naar de adresbalk. Ik blokkeer trackers en werk de beveiliging van je verbinding bij wanneer mogelijk 🔒" + } + }, + "pl" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Wszystko gotowe! W każdej chwili możesz mnie znaleźć w Docku.\nChcesz zobaczyć, jak Cię chronię? Spróbuj odwiedzić jedną z ulubionych stron 👆\n\nW międzyczasie obserwuj pasek adresu. Będę blokować mechanizmy śledzące i w miarę możliwości poprawiać bezpieczeństwo połączenia 🔒" + } + }, + "pt" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Está tudo pronto! Podes encontrar-me na Dock em qualquer altura.\nQueres ver como te protejo? Experimenta visitar um dos teus sites favoritos 👆\n\nContinua a observar a barra de endereço à medida que vais avançando. Vou bloquear os rastreadores e melhorar a segurança da tua ligação sempre que possível 🔒" + } + }, + "ru" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Готово! Теперь меня всегда можно найти на док-панели.\nВам интересно, как я защищаю вашу конфиденциальность? Зайдите на свой любимый сайт...👆\n\nИ следите за адресной строкой. По возможности я заблокирую все трекеры и сделаю соединение более безопасным {00A0}🔒" + } + } + } + }, "onboarding.startbrowsing.text" : { "comment" : "Call to action to start using the app as a browser", "extractionState" : "extracted_with_value", @@ -43887,6 +44307,66 @@ } } }, + "preferences.add-to-dock" : { + "comment" : "Action button to add the app to the Dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zum Dock hinzufügen …" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add to Dock…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir al Dock…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter au Dock…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi al dock…" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "App toevoegen aan je dock…" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj do Docka…" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar à Dock…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить на док-панель…" + } + } + } + }, "preferences.always-on" : { "comment" : "Status indicator of a browser privacy protection feature.", "extractionState" : "extracted_with_value", @@ -45267,6 +45747,66 @@ } } }, + "preferences.is-added-to-dock" : { + "comment" : "Indicates that the browser is added to the macOS system Dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo wird zum Dock hinzugefügt." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo is added to the Dock." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo se ha añadido al Dock." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo a été ajouté au Dock." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo è stato aggiunto al dock." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo is toegevoegd aan het Dock." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przeglądarka DuckDuckGo została dodana do Docka." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O DuckDuckGo foi adicionado à Dock." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык DuckDuckGo добавлен на док-панель." + } + } + } + }, "preferences.main-settings" : { "comment" : "Section header in Preferences for main settings", "extractionState" : "extracted_with_value", @@ -45327,6 +45867,66 @@ } } }, + "preferences.not-added-to-dock" : { + "comment" : "Indicate that the browser is not added to macOS system Dock", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo wird nicht zum Dock hinzugefügt." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo is not added to the Dock." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo no se ha añadido al Dock." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo n'a pas été ajouté au Dock." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo non è stato aggiunto al dock." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo is niet toegevoegd aan het Dock." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przeglądarka DuckDuckGo nie została dodana do Docka." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O DuckDuckGo não foi adicionado à Dock." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлык DuckDuckGo не добавлен на док-панель." + } + } + } + }, "preferences.off" : { "comment" : "Status indicator of a browser privacy protection feature.", "extractionState" : "extracted_with_value", @@ -45687,6 +46287,66 @@ } } }, + "preferences.shortcuts" : { + "comment" : "Name of the preferences section related to shortcuts", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shortcuts" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Shortcuts" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesos directos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Raccourcis" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scorciatoie" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sneltoetsen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skróty" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atalhos" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ярлыки" + } + } + } + }, "preferences.show-home" : { "comment" : "Option to control session startup", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/Onboarding/View/OnboardingFlow.swift b/DuckDuckGo/Onboarding/View/OnboardingFlow.swift index 721f60bb88..23b2179eaf 100644 --- a/DuckDuckGo/Onboarding/View/OnboardingFlow.swift +++ b/DuckDuckGo/Onboarding/View/OnboardingFlow.swift @@ -30,6 +30,14 @@ struct OnboardingFlow: View { @State var daxInSpeechPosition = false @State var showDialogs = false + var startBrowsingText: String { + if model.addToDockPressed { + return UserText.onboardingStartBrowsingAddedToDockText + } else { + return UserText.onboardingStartBrowsingText + } + } + var body: some View { VStack(alignment: daxInSpeechPosition ? .leading : .center) { @@ -62,7 +70,14 @@ struct OnboardingFlow: View { model.onSetDefaultSkipped() }.visibility(model.state == .setDefault ? .visible : .gone) - DaxSpeech(text: UserText.onboardingStartBrowsingText, onTypingFinished: nil) + ActionSpeech(text: UserText.onboardingAddToDockText, + actionName: UserText.onboardingAddToDockButton) { + model.onAddToDockPressed() + } skip: { + model.onAddToDockSkipped() + }.visibility(model.state == .addToDock ? .visible : .gone) + + DaxSpeech(text: startBrowsingText, onTypingFinished: nil) .visibility(model.state == .startBrowsing ? .visible : .gone) }.visibility(showDialogs ? .visible : .gone) diff --git a/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift b/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift index e9448763fc..8a38258b23 100644 --- a/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift +++ b/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift @@ -17,6 +17,7 @@ // import SwiftUI +import PixelKit protocol OnboardingDelegate: NSObjectProtocol { @@ -26,6 +27,9 @@ protocol OnboardingDelegate: NSObjectProtocol { /// Request set default should be launched. Whatever happens, call the completion to move on to the next screen. func onboardingDidRequestSetDefault(completion: @escaping () -> Void) + /// Adding to the Dock should be launched. Whatever happens, call the completion to move on to the next screen. + func onboardingDidRequestAddToDock(completion: @escaping () -> Void) + /// Has finished, but still showing a screen. This is when to re-enable the UI. func onboardingHasFinished() @@ -39,16 +43,27 @@ final class OnboardingViewModel: ObservableObject { case welcome case importData case setDefault + case addToDock case startBrowsing } var typingDisabled = false + var addToDockPressed = false @Published var skipTypingRequested = false @Published var state: OnboardingPhase = .startFlow { didSet { skipTypingRequested = false + + if state == .addToDock { + PixelKit.fire(GeneralPixel.addToDockOnboardingStepPresented, + includeAppVersionParameter: false) + } + if state == .startBrowsing { + PixelKit.fire(GeneralPixel.startBrowsingOnboardingStepPresented, + includeAppVersionParameter: false) + } } } @@ -105,14 +120,43 @@ final class OnboardingViewModel: ObservableObject { @MainActor func onSetDefaultPressed() { delegate?.onboardingDidRequestSetDefault { [weak self] in +#if !APPSTORE + self?.state = .addToDock +#else self?.state = .startBrowsing Self.isOnboardingFinished = true self?.delegate?.onboardingHasFinished() +#endif } } @MainActor func onSetDefaultSkipped() { +#if !APPSTORE + state = .addToDock +#else + state = .startBrowsing + Self.isOnboardingFinished = true + delegate?.onboardingHasFinished() +#endif + } + + @MainActor + func onAddToDockPressed() { + PixelKit.fire(GeneralPixel.userAddedToDockDuringOnboarding, + includeAppVersionParameter: false) + addToDockPressed = true + delegate?.onboardingDidRequestAddToDock { [weak self] in + self?.state = .startBrowsing + Self.isOnboardingFinished = true + self?.delegate?.onboardingHasFinished() + } + } + + @MainActor + func onAddToDockSkipped() { + PixelKit.fire(GeneralPixel.userSkippedAddingToDockFromOnboarding, + includeAppVersionParameter: false) state = .startBrowsing Self.isOnboardingFinished = true delegate?.onboardingHasFinished() diff --git a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift index a5495827fe..45d7bf74f7 100644 --- a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift @@ -24,6 +24,7 @@ import PixelKit protocol DefaultBrowserProvider { var bundleIdentifier: String { get } + var defaultBrowserURL: URL? { get } var isDefault: Bool { get } func presentDefaultBrowserPrompt() throws func openSystemPreferences() @@ -37,8 +38,12 @@ struct SystemDefaultBrowserProvider: DefaultBrowserProvider { let bundleIdentifier: String + var defaultBrowserURL: URL? { + return NSWorkspace.shared.urlForApplication(toOpen: URL(string: "http://")!) + } + var isDefault: Bool { - guard let defaultBrowserURL = NSWorkspace.shared.urlForApplication(toOpen: URL(string: "http://")!), + guard let defaultBrowserURL = defaultBrowserURL, let ddgBrowserURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) else { return false diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index 41e92b6886..956df10da6 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -21,6 +21,7 @@ import Combine import PreferencesViews import SwiftUI import SwiftUIExtensions +import PixelKit extension Preferences { @@ -31,11 +32,48 @@ extension Preferences { @ObservedObject var tabsModel: TabsPreferences @ObservedObject var dataClearingModel: DataClearingPreferences @State private var showingCustomHomePageSheet = false + @State private var isAddedToDock = false + var dockCustomizer: DockCustomizer var body: some View { PreferencePane(UserText.general) { - // SECTION 1: On Startup + // SECTION 1: Shortcuts +#if !APPSTORE + PreferencePaneSection(UserText.shortcuts, spacing: 4) { + PreferencePaneSubSection { + HStack { + if isAddedToDock || dockCustomizer.isAddedToDock { + HStack { + Image(.successCheckmark) + Text(UserText.isAddedToDock) + } + .transition(.opacity) + .padding(.trailing, 8) + } else { + HStack { + Image(.warning).foregroundColor(Color(.linkBlue)) + Text(UserText.isNotAddedToDock) + } + .padding(.trailing, 8) + Button(action: { + withAnimation { + PixelKit.fire(GeneralPixel.userAddedToDockFromSettings, + includeAppVersionParameter: false) + dockCustomizer.addToDock() + isAddedToDock = true + } + }) { + Text(UserText.addToDock) + .fixedSize(horizontal: true, vertical: false) + .multilineTextAlignment(.center) + } + } + } + } + } +#endif + // SECTION 2: On Startup PreferencePaneSection(UserText.onStartup) { PreferencePaneSubSection { @@ -61,7 +99,7 @@ extension Preferences { } } - // SECTION 2: Tabs + // SECTION 3: Tabs PreferencePaneSection(UserText.tabs) { PreferencePaneSubSection { ToggleMenuItem(UserText.preferNewTabsToWindows, isOn: $tabsModel.preferNewTabsToWindows) @@ -80,7 +118,7 @@ extension Preferences { } } - // SECTION 3: Home Page + // SECTION 4: Home Page PreferencePaneSection(UserText.homePage) { PreferencePaneSubSection { @@ -124,12 +162,12 @@ extension Preferences { CustomHomePageSheet(startupModel: startupModel, isSheetPresented: $showingCustomHomePageSheet) } - // SECTION 4: Search Settings + // SECTION 5: Search Settings PreferencePaneSection(UserText.privateSearch) { ToggleMenuItem(UserText.showAutocompleteSuggestions, isOn: $searchModel.showAutocompleteSuggestions).accessibilityIdentifier("PreferencesGeneralView.showAutocompleteSuggestions") } - // SECTION 5: Downloads + // SECTION 6: Downloads PreferencePaneSection(UserText.downloads) { PreferencePaneSubSection { ToggleMenuItem(UserText.downloadsOpenPopupOnCompletion, diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 1f4fc03135..350eb84f96 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -89,7 +89,8 @@ enum Preferences { downloadsModel: DownloadsPreferences.shared, searchModel: SearchPreferences.shared, tabsModel: TabsPreferences.shared, - dataClearingModel: DataClearingPreferences.shared) + dataClearingModel: DataClearingPreferences.shared, + dockCustomizer: DockCustomizer()) case .sync: SyncView() case .appearance: diff --git a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift index 84219ba446..19e7c70108 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift @@ -62,6 +62,7 @@ final class StatisticsLoader { } PixelKit.fire(GeneralPixel.serp) self.fireDailyOsVersionCounterPixel() + self.fireDockPixel() } else if !self.statisticsStore.isAppRetentionFiredToday { self.refreshAppRetentionAtb(completion: completion) } else { @@ -231,4 +232,13 @@ final class StatisticsLoader { } } + private func fireDockPixel() { + DispatchQueue.global().asyncAfter(deadline: .now() + Double.random(in: 0.5...5)) { + if DockCustomizer().isAddedToDock { + PixelKit.fire(GeneralPixel.serpAddedToDock, + includeAppVersionParameter: false) + } + } + } + } diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 0380cedd98..d5846e38a7 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -146,6 +146,16 @@ enum GeneralPixel: PixelKitEventV2 { case defaultRequestedFromSettings case defaultRequestedFromOnboarding + // Adding to the Dock + case addToDockOnboardingStepPresented + case userAddedToDockDuringOnboarding + case userSkippedAddingToDockFromOnboarding + case startBrowsingOnboardingStepPresented + case addToDockNewTabPageCardPresented + case userAddedToDockFromNewTabPageCard + case userAddedToDockFromSettings + case serpAddedToDock + case protectionToggledOffBreakageReport case toggleProtectionsDailyCount case toggleReportDoNotSend @@ -530,6 +540,15 @@ enum GeneralPixel: PixelKitEventV2 { case .defaultRequestedFromSettings: return "m_mac_default_requested_from_settings" case .defaultRequestedFromOnboarding: return "m_mac_default_requested_from_onboarding" + case .addToDockOnboardingStepPresented: return "m_mac_add_to_dock_onboarding_step_presented" + case .userAddedToDockDuringOnboarding: return "m_mac_user_added_to_dock_during_onboarding" + case .userSkippedAddingToDockFromOnboarding: return "m_mac_user_skipped_adding_to_dock_from_onboarding" + case .startBrowsingOnboardingStepPresented: return "m_mac_start_browsing_onboarding_step_presented" + case .addToDockNewTabPageCardPresented: return "m_mac_add_to_dock_new_tab_page_card_presented_u" + case .userAddedToDockFromNewTabPageCard: return "m_mac_user_added_to_dock_from_new_tab_page_card" + case .userAddedToDockFromSettings: return "m_mac_user_added_to_dock_from_settings" + case .serpAddedToDock: return "m_mac_serp_added_to_dock" + case .protectionToggledOffBreakageReport: return "m_mac_protection-toggled-off-breakage-report" case .toggleProtectionsDailyCount: return "m_mac_toggle-protections-daily-count" case .toggleReportDoNotSend: return "m_mac_toggle-report-do-not-send" diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index dde4a10a29..b785424d39 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -41,6 +41,7 @@ final class BrowserTabViewController: NSViewController { private let tabCollectionViewModel: TabCollectionViewModel private let bookmarkManager: BookmarkManager + private let dockCustomizer = DockCustomizer() private var tabViewModelCancellables = Set() private var activeUserDialogCancellable: Cancellable? @@ -1139,6 +1140,11 @@ extension BrowserTabViewController: OnboardingDelegate { } } + func onboardingDidRequestAddToDock(completion: @escaping () -> Void) { + dockCustomizer.addToDock() + completion() + } + func onboardingHasFinished() { (view.window?.windowController as? MainWindowController)?.userInteraction(prevented: false) } diff --git a/UnitTests/App/DockCustomizerMock.swift b/UnitTests/App/DockCustomizerMock.swift new file mode 100644 index 0000000000..5e8812bad0 --- /dev/null +++ b/UnitTests/App/DockCustomizerMock.swift @@ -0,0 +1,39 @@ +// +// DockCustomizerMock.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 + +class DockCustomizerMock: DockCustomization { + private var dockStatus: Bool = false + + var isAddedToDock: Bool { + return dockStatus + } + + @discardableResult + func addToDock() -> Bool { + if !dockStatus { + dockStatus = true + return true + } else { + return false + } + } +} diff --git a/UnitTests/App/DockPositionProviderTests.swift b/UnitTests/App/DockPositionProviderTests.swift new file mode 100644 index 0000000000..d7eb2895cb --- /dev/null +++ b/UnitTests/App/DockPositionProviderTests.swift @@ -0,0 +1,48 @@ +// +// DockPositionProviderTests.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 + +class DockPositionProviderTests: XCTestCase { + + var provider: DockPositionProvider! + var mockBrowserProvider: DefaultBrowserProviderMock! + + override func setUp() { + super.setUp() + mockBrowserProvider = DefaultBrowserProviderMock() + provider = DockPositionProvider(defaultBrowserProvider: mockBrowserProvider) + } + + override func tearDown() { + provider = nil + mockBrowserProvider = nil + super.tearDown() + } + + func testWhenNotDefaultBrowser_ThenIndexIsNextToDefault() { + mockBrowserProvider.isDefault = false + mockBrowserProvider.defaultBrowserURL = URL(string: "file:///Applications/Firefox.app/")! + let currentApps = [URL(string: "file:///Applications/Safari.app/")!, URL(string: "file:///Applications/Firefox.app/")!, URL(string: "file:///Applications/Arc.app/")!] + let index = provider.newDockIndex(from: currentApps) + + XCTAssertEqual(index, 2, "The new app should be placed next to default browser.") + } + +} diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index f7c2060ffc..9d49ab4073 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -69,6 +69,7 @@ final class ContinueSetUpModelTests: XCTestCase { var coookiePopupProtectionPreferences: MockCookiePopupProtectionPreferencesPersistor! var privacyConfigManager: MockPrivacyConfigurationManager! var randomNumberGenerator: MockRandomNumberGenerator! + var dockCustomizer: DockCustomization! let userDefaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).\(NSApplication.runType)")! @MainActor override func setUp() { @@ -86,6 +87,7 @@ final class ContinueSetUpModelTests: XCTestCase { let config = MockPrivacyConfiguration() privacyConfigManager.privacyConfig = config randomNumberGenerator = MockRandomNumberGenerator() + dockCustomizer = DockCustomizerMock() #if DBP let messaging = HomePageRemoteMessaging( @@ -103,6 +105,7 @@ final class ContinueSetUpModelTests: XCTestCase { vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, @@ -146,6 +149,7 @@ final class ContinueSetUpModelTests: XCTestCase { vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, @@ -226,6 +230,7 @@ final class ContinueSetUpModelTests: XCTestCase { @MainActor func testWhenAskedToPerformActionForImportPromptThrowsThenItOpensImportWindow() { let numberOfFeatures = HomePage.Models.FeatureType.allCases.count - 1 + vm.shouldShowAllFeatures = true XCTAssertEqual(vm.visibleFeaturesMatrix.flatMap { $0 }.count, numberOfFeatures) @@ -353,10 +358,12 @@ final class ContinueSetUpModelTests: XCTestCase { emailStorage.isEmailProtectionEnabled = true duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true capturingDataImportProvider.didImport = true + dockCustomizer.addToDock() userDefaults.set(false, forKey: UserDefaultsWrapper.Key.homePageShowPermanentSurvey.rawValue) vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, @@ -385,6 +392,11 @@ final class ContinueSetUpModelTests: XCTestCase { vm.removeItem(for: .emailProtection) XCTAssertFalse(vm.visibleFeaturesMatrix.flatMap { $0 }.contains(.emailProtection)) +#if !APPSTORE + vm.removeItem(for: .dock) + XCTAssertFalse(vm.visibleFeaturesMatrix.flatMap { $0 }.contains(.dock)) +#endif + let vm2 = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults) XCTAssertTrue(vm2.visibleFeaturesMatrix.flatMap { $0 }.isEmpty) } @@ -461,6 +473,7 @@ final class ContinueSetUpModelTests: XCTestCase { userDefaults.set(true, forKey: UserDefaultsWrapper.Key.homePageShowPermanentSurvey.rawValue) let vm = HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: capturingDefaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, tabCollectionViewModel: tabCollectionVM, emailManager: emailManager, @@ -512,6 +525,27 @@ final class ContinueSetUpModelTests: XCTestCase { #endif } + @MainActor func test_WhenUserDoesntHaveApplicationInTheDock_ThenAddToDockCardIsDisplayed() { +#if !APPSTORE + let dockCustomizer = DockCustomizerMock() + + let vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults, dockCustomizer: dockCustomizer) + vm.shouldShowAllFeatures = true + + XCTAssert(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.dock)) +#endif + } + + @MainActor func test_WhenUserHasApplicationInTheDock_ThenAddToDockCardIsNotDisplayed() { + let dockCustomizer = DockCustomizerMock() + dockCustomizer.addToDock() + + let vm = HomePage.Models.ContinueSetUpModel.fixture(appGroupUserDefaults: userDefaults, dockCustomizer: dockCustomizer) + vm.shouldShowAllFeatures = true + + XCTAssertFalse(vm.visibleFeaturesMatrix.reduce([], +).contains(HomePage.Models.FeatureType.dock)) + } + } extension HomePage.Models.ContinueSetUpModel { @@ -523,7 +557,8 @@ extension HomePage.Models.ContinueSetUpModel { privacyConfig: MockPrivacyConfiguration = MockPrivacyConfiguration(), appGroupUserDefaults: UserDefaults, permanentSurveyManager: MockPermanentSurveyManager = MockPermanentSurveyManager(), - randomNumberGenerator: RandomNumberGenerating = MockRandomNumberGenerator() + randomNumberGenerator: RandomNumberGenerating = MockRandomNumberGenerator(), + dockCustomizer: DockCustomization = DockCustomizerMock() ) -> HomePage.Models.ContinueSetUpModel { privacyConfig.featureSettings = [ "networkProtection": "disabled" @@ -547,6 +582,7 @@ extension HomePage.Models.ContinueSetUpModel { return HomePage.Models.ContinueSetUpModel( defaultBrowserProvider: defaultBrowserProvider, + dockCustomizer: dockCustomizer, dataImportProvider: dataImportProvider, tabCollectionViewModel: TabCollectionViewModel(), emailManager: emailManager, diff --git a/UnitTests/HomePage/Mocks/CapturingDefaultBrowserProvider.swift b/UnitTests/HomePage/Mocks/CapturingDefaultBrowserProvider.swift index 9931c9de94..dc544f2e5b 100644 --- a/UnitTests/HomePage/Mocks/CapturingDefaultBrowserProvider.swift +++ b/UnitTests/HomePage/Mocks/CapturingDefaultBrowserProvider.swift @@ -21,6 +21,8 @@ import Foundation @testable import DuckDuckGo_Privacy_Browser class CapturingDefaultBrowserProvider: DefaultBrowserProvider { + var defaultBrowserURL: URL? + var presentDefaultBrowserPromptCalled = false var openSystemPreferencesCalled = false var throwError = false diff --git a/UnitTests/Onboarding/OnboardingTests.swift b/UnitTests/Onboarding/OnboardingTests.swift index f0366d6498..a123262061 100644 --- a/UnitTests/Onboarding/OnboardingTests.swift +++ b/UnitTests/Onboarding/OnboardingTests.swift @@ -43,13 +43,23 @@ class OnboardingTests: XCTestCase { assertStateChange(model, .startFlow, .welcome, model.onSplashFinished) assertStateChange(model, .welcome, .importData, model.onStartPressed) assertStateChange(model, .importData, .setDefault, model.onImportPressed) +#if APPSTORE assertStateChange(model, .setDefault, .startBrowsing, model.onSetDefaultPressed) +#else + assertStateChange(model, .setDefault, .addToDock, model.onSetDefaultPressed) + assertStateChange(model, .addToDock, .startBrowsing, model.onAddToDockPressed) +#endif model.state = .importData assertStateChange(model, .importData, .setDefault, model.onImportSkipped) model.state = .setDefault +#if APPSTORE assertStateChange(model, .setDefault, .startBrowsing, model.onSetDefaultSkipped) +#else + assertStateChange(model, .setDefault, .addToDock, model.onSetDefaultSkipped) + assertStateChange(model, .addToDock, .startBrowsing, model.onAddToDockSkipped) +#endif } func testWhenImportPressedDelegateIsCalled() { @@ -79,12 +89,37 @@ class OnboardingTests: XCTestCase { model.onSetDefaultSkipped() XCTAssertEqual(0, delegate.didRequestImportDataCalled) XCTAssertEqual(0, delegate.didRequestSetDefaultCalled) + XCTAssertEqual(0, delegate.didRequestAddToDockCalled) +#if APPSTORE XCTAssertEqual(1, delegate.hasFinishedCalled) +#else + XCTAssertEqual(0, delegate.hasFinishedCalled) +#endif model.onSetDefaultPressed() XCTAssertEqual(0, delegate.didRequestImportDataCalled) + XCTAssertEqual(0, delegate.didRequestAddToDockCalled) XCTAssertEqual(1, delegate.didRequestSetDefaultCalled) +#if APPSTORE XCTAssertEqual(2, delegate.hasFinishedCalled) +#else + XCTAssertEqual(0, delegate.hasFinishedCalled) +#endif + +#if !APPSTORE + model.onAddToDockSkipped() + XCTAssertEqual(0, delegate.didRequestImportDataCalled) + XCTAssertEqual(1, delegate.didRequestSetDefaultCalled) + XCTAssertEqual(0, delegate.didRequestAddToDockCalled) + XCTAssertEqual(1, delegate.hasFinishedCalled) +#endif + +#if !APPSTORE + model.onAddToDockPressed() + XCTAssertEqual(0, delegate.didRequestImportDataCalled) + XCTAssertEqual(1, delegate.didRequestSetDefaultCalled) + XCTAssertEqual(2, delegate.hasFinishedCalled) +#endif XCTAssertTrue(onboardingFinished) } @@ -114,6 +149,7 @@ class OnboardingTests: XCTestCase { final class MockOnboardingDelegate: NSObject, OnboardingDelegate { var didRequestImportDataCalled = 0 var didRequestSetDefaultCalled = 0 + var didRequestAddToDockCalled = 0 var hasFinishedCalled = 0 func onboardingDidRequestImportData(completion: @escaping () -> Void) { @@ -126,6 +162,11 @@ final class MockOnboardingDelegate: NSObject, OnboardingDelegate { completion() } + func onboardingDidRequestAddToDock(completion: @escaping () -> Void) { + didRequestAddToDockCalled += 1 + completion() + } + func onboardingHasFinished() { hasFinishedCalled += 1 } diff --git a/UnitTests/Preferences/DefaultBrowserPreferencesTests.swift b/UnitTests/Preferences/DefaultBrowserPreferencesTests.swift index c7e2aae416..d8106d7574 100644 --- a/UnitTests/Preferences/DefaultBrowserPreferencesTests.swift +++ b/UnitTests/Preferences/DefaultBrowserPreferencesTests.swift @@ -20,11 +20,13 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser final class DefaultBrowserProviderMock: DefaultBrowserProvider { + enum MockError: Error { case generic } var bundleIdentifier: String = "com.duckduckgo.DefaultBrowserPreferencesTests" + var defaultBrowserURL: URL? var isDefault: Bool = false var _presentDefaultBrowserPrompt: () throws -> Void = {} var _openSystemPreferences: () -> Void = {}