From efbcf8b38a1780ebc870e83fb6b7ce9a9e53ae6a Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 13 Nov 2023 08:46:50 +0100 Subject: [PATCH 1/4] Properly refresh home page favorites view when updating favorites display mode (#2148) Task/Issue URL: https://app.asana.com/0/1201493110486074/1205918060547867/f Description: Listen to changes to favorites display mode also in HomeViewController and reload favorites view as needed. --- DuckDuckGo/HomeViewController.swift | 44 +++++++++++++++++++++++++---- DuckDuckGo/MainViewController.swift | 3 +- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/HomeViewController.swift b/DuckDuckGo/HomeViewController.swift index 4f97f8c46b..c90a9ac317 100644 --- a/DuckDuckGo/HomeViewController.swift +++ b/DuckDuckGo/HomeViewController.swift @@ -63,42 +63,64 @@ class HomeViewController: UIViewController { private let tabModel: Tab private let favoritesViewModel: FavoritesListInteracting + private let appSettings: AppSettings private var viewModelCancellable: AnyCancellable? + private var favoritesDisplayModeCancellable: AnyCancellable? #if APP_TRACKING_PROTECTION private let appTPHomeViewModel: AppTPHomeViewModel #endif #if APP_TRACKING_PROTECTION - static func loadFromStoryboard(model: Tab, favoritesViewModel: FavoritesListInteracting, appTPDatabase: CoreDataDatabase) -> HomeViewController { + static func loadFromStoryboard( + model: Tab, + favoritesViewModel: FavoritesListInteracting, + appSettings: AppSettings, + appTPDatabase: CoreDataDatabase + ) -> HomeViewController { + let storyboard = UIStoryboard(name: "Home", bundle: nil) let controller = storyboard.instantiateViewController(identifier: "HomeViewController", creator: { coder in - HomeViewController(coder: coder, tabModel: model, favoritesViewModel: favoritesViewModel, appTPDatabase: appTPDatabase) + HomeViewController( + coder: coder, + tabModel: model, + favoritesViewModel: favoritesViewModel, + appSettings: appSettings, + appTPDatabase: appTPDatabase + ) }) return controller } #else - static func loadFromStoryboard(model: Tab, favoritesViewModel: FavoritesListInteracting) -> HomeViewController { + static func loadFromStoryboard(model: Tab, favoritesViewModel: FavoritesListInteracting, appSettings: AppSettings) -> HomeViewController { let storyboard = UIStoryboard(name: "Home", bundle: nil) let controller = storyboard.instantiateViewController(identifier: "HomeViewController", creator: { coder in - HomeViewController(coder: coder, tabModel: model, favoritesViewModel: favoritesViewModel) + HomeViewController(coder: coder, tabModel: model, favoritesViewModel: favoritesViewModel, appSettings: appSettings) }) return controller } #endif #if APP_TRACKING_PROTECTION - required init?(coder: NSCoder, tabModel: Tab, favoritesViewModel: FavoritesListInteracting, appTPDatabase: CoreDataDatabase) { + required init?( + coder: NSCoder, + tabModel: Tab, + favoritesViewModel: FavoritesListInteracting, + appSettings: AppSettings, + appTPDatabase: CoreDataDatabase + ) { self.tabModel = tabModel self.favoritesViewModel = favoritesViewModel + self.appSettings = appSettings self.appTPHomeViewModel = AppTPHomeViewModel(appTrackingProtectionDatabase: appTPDatabase) super.init(coder: coder) } #else - required init?(coder: NSCoder, tabModel: Tab, favoritesViewModel: FavoritesListInteracting) { + required init?(coder: NSCoder, tabModel: Tab, favoritesViewModel: FavoritesListInteracting, appSettings: AppSettings) { self.tabModel = tabModel self.favoritesViewModel = favoritesViewModel + self.appSettings = appSettings super.init(coder: coder) } @@ -133,6 +155,16 @@ class HomeViewController: UIViewController { self.delegate?.home(self, didRequestHideLogo: false) } } + + favoritesDisplayModeCancellable = NotificationCenter.default.publisher(for: AppUserDefaults.Notifications.favoritesDisplayModeChange) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self else { + return + } + self.favoritesViewModel.favoritesDisplayMode = self.appSettings.favoritesDisplayMode + self.collectionView.reloadData() + } } @objc func bookmarksDidChange() { diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index ebd8cfd37a..bbc4f9f9d3 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -645,9 +645,10 @@ class MainViewController: UIViewController { #if APP_TRACKING_PROTECTION let controller = HomeViewController.loadFromStoryboard(model: tabModel!, favoritesViewModel: favoritesViewModel, + appSettings: appSettings, appTPDatabase: appTrackingProtectionDatabase) #else - let controller = HomeViewController.loadFromStoryboard(model: tabModel!, favoritesViewModel: favoritesViewModel) + let controller = HomeViewController.loadFromStoryboard(model: tabModel!, favoritesViewModel: favoritesViewModel, appSettings: appSettings) #endif homeController = controller From 7b58cdb0fd6cab2fe36bba45a5547f431d655c4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 12:59:21 +0100 Subject: [PATCH 2/4] Bump submodules/privacy-reference-tests from `0d23f76` to `7519c3d` (#2135) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Graeme Arthur --- submodules/privacy-reference-tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/privacy-reference-tests b/submodules/privacy-reference-tests index 0d23f76801..7519c3d430 160000 --- a/submodules/privacy-reference-tests +++ b/submodules/privacy-reference-tests @@ -1 +1 @@ -Subproject commit 0d23f76801c2e73ae7d5ed7daa4af4aca5beec73 +Subproject commit 7519c3d430e5dcef75b6128bfdadb0de3f463a49 From 20176c44a2c241fddbc9f8eb72dd460fe63b0457 Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Mon, 13 Nov 2023 12:54:32 -0600 Subject: [PATCH 3/4] Normalize ampUrl in breakage pixel (#2154) --- DuckDuckGo/BrokenSiteInfo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/BrokenSiteInfo.swift b/DuckDuckGo/BrokenSiteInfo.swift index 9a1ed49e6e..fc516b48ef 100644 --- a/DuckDuckGo/BrokenSiteInfo.swift +++ b/DuckDuckGo/BrokenSiteInfo.swift @@ -104,7 +104,7 @@ public struct BrokenSiteInfo { Keys.manufacturer: manufacturer, Keys.model: model, Keys.gpc: gpc ? "true" : "false", - Keys.ampUrl: ampUrl ?? "", + Keys.ampUrl: normalize(URL(string: ampUrl ?? "")), Keys.urlParametersRemoved: urlParametersRemoved ? "true" : "false", Keys.protectionsState: protectionsState ? "true" : "false" ] From 7243cb683ccc8e44edf1fe921c69241811acd53e Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 13 Nov 2023 15:40:54 -0800 Subject: [PATCH 4/4] Add NetP widget (#2142) Task/Issue URL: https://app.asana.com/0/414235014887631/1205921206830836/f Tech Design URL: CC: @graeme Description: This PR adds a NetP widget. It's only available in the debug and alpha builds, and only available on iOS 17. --- .github/workflows/alpha.yml | 3 +- Core/AppDeepLinkSchemes.swift | 2 + Core/UserDefaults+NetworkProtection.swift | 6 + DuckDuckGo.xcodeproj/project.pbxproj | 42 ++- DuckDuckGo/AppDelegate+AppDeepLinks.swift | 6 + DuckDuckGo/AppDelegate.swift | 22 +- .../NetworkProtectionStatusViewModel.swift | 3 + .../NetworkProtectionWidgetRefreshModel.swift | 46 +++ DuckDuckGo/VPNIntents.swift | 102 ++++++ ...etworkProtectionPacketTunnelProvider.swift | 14 +- .../vpn-off.imageset/Contents.json | 12 + .../vpn-off.imageset/vpn-off.pdf | Bin 0 -> 8715 bytes .../vpn-on.imageset/Contents.json | 12 + .../vpn-on.imageset/vpn-on.pdf | Bin 0 -> 9104 bytes Widgets/DeepLinks.swift | 1 + Widgets/VPNWidget.swift | 301 ++++++++++++++++++ Widgets/Widgets.swift | 7 + Widgets/WidgetsExtension.entitlements | 1 + WidgetsExtensionAlpha.entitlements | 5 +- 19 files changed, 578 insertions(+), 7 deletions(-) create mode 100644 DuckDuckGo/NetworkProtectionWidgetRefreshModel.swift create mode 100644 DuckDuckGo/VPNIntents.swift create mode 100644 Widgets/Assets.xcassets/vpn-off.imageset/Contents.json create mode 100644 Widgets/Assets.xcassets/vpn-off.imageset/vpn-off.pdf create mode 100644 Widgets/Assets.xcassets/vpn-on.imageset/Contents.json create mode 100644 Widgets/Assets.xcassets/vpn-on.imageset/vpn-on.pdf create mode 100644 Widgets/VPNWidget.swift diff --git a/.github/workflows/alpha.yml b/.github/workflows/alpha.yml index 3d3ba3c8af..01ec4a7540 100644 --- a/.github/workflows/alpha.yml +++ b/.github/workflows/alpha.yml @@ -74,8 +74,9 @@ jobs: restore-keys: | ${{ runner.os }}-spm- + # Using Xcode 15 as the alpha build uses iOS 17 APIs - name: Select Xcode - run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_15.0.1.app/Contents/Developer - name: Prepare fastlane run: bundle install diff --git a/Core/AppDeepLinkSchemes.swift b/Core/AppDeepLinkSchemes.swift index c88f697cbc..38fa976b99 100644 --- a/Core/AppDeepLinkSchemes.swift +++ b/Core/AppDeepLinkSchemes.swift @@ -31,6 +31,8 @@ public enum AppDeepLinkSchemes: String, CaseIterable { case addFavorite = "ddgAddFavorite" + case openVPN = "ddgOpenVPN" + public var url: URL { URL(string: rawValue + "://")! } diff --git a/Core/UserDefaults+NetworkProtection.swift b/Core/UserDefaults+NetworkProtection.swift index fb9def1004..e137aa51c2 100644 --- a/Core/UserDefaults+NetworkProtection.swift +++ b/Core/UserDefaults+NetworkProtection.swift @@ -31,4 +31,10 @@ public extension UserDefaults { } } +public enum NetworkProtectionUserDefaultKeys { + + public static let lastSelectedServer = "com.duckduckgo.network-protection.last-selected-server" + +} + #endif diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index aace48e673..0a6878724b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -267,6 +267,7 @@ 37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FCAABF29930E26000E420A /* FailedAssertionView.swift */; }; 37FD780F2A29E28B00B36DB1 /* SyncErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD780E2A29E28B00B36DB1 /* SyncErrorHandler.swift */; }; 4B0295192537BC6700E00CEF /* ConfigurationDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0295182537BC6700E00CEF /* ConfigurationDebugViewController.swift */; }; + 4B274F602AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B274F5F2AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift */; }; 4B2754EC29E8C7DF00394032 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2754EB29E8C7DF00394032 /* Lottie */; }; 4B470ED6299C49800086EBDC /* AppTrackingProtectionDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B470ED5299C49800086EBDC /* AppTrackingProtectionDatabase.swift */; }; 4B470ED9299C4AED0086EBDC /* AppTrackingProtectionModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 4B470ED7299C4AED0086EBDC /* AppTrackingProtectionModel.xcdatamodeld */; }; @@ -275,6 +276,8 @@ 4B470EE4299C6DFB0086EBDC /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F143C2E41E4A4CD400CFDE3A /* Core.framework */; }; 4B52648B25F9613B00CB4C24 /* trackerData.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B52648A25F9613B00CB4C24 /* trackerData.json */; }; 4B53648A26718D0E001AA041 /* EmailWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B53648926718D0E001AA041 /* EmailWaitlist.swift */; }; + 4B5C462A2AF2A6E6002A4432 /* VPNIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5C46292AF2A6E6002A4432 /* VPNIntents.swift */; }; + 4B5C462B2AF2BDC4002A4432 /* VPNIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5C46292AF2A6E6002A4432 /* VPNIntents.swift */; }; 4B60AC97252EC07B00E8D219 /* fullscreenvideo.js in Resources */ = {isa = PBXBuildFile; fileRef = 4B60AC96252EC07B00E8D219 /* fullscreenvideo.js */; }; 4B60ACA1252EC0B100E8D219 /* FullScreenVideoUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B60ACA0252EC0B100E8D219 /* FullScreenVideoUserScript.swift */; }; 4B62C4BA25B930DD008912C6 /* AppConfigurationFetchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B62C4B925B930DD008912C6 /* AppConfigurationFetchTests.swift */; }; @@ -291,6 +294,8 @@ 4B83397329AFB8D2003F7EA9 /* AppTrackingProtectionFeedbackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B83397229AFB8D2003F7EA9 /* AppTrackingProtectionFeedbackModel.swift */; }; 4B83397529AFBCE6003F7EA9 /* AppTrackingProtectionFeedbackModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B83397429AFBCE6003F7EA9 /* AppTrackingProtectionFeedbackModelTests.swift */; }; 4B948E2629DCCDB9002531FA /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 4B948E2529DCCDB9002531FA /* Persistence */; }; + 4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */; }; + 4BBBBA872B02E85400D965DA /* DesignResourcesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4BBBBA862B02E85400D965DA /* DesignResourcesKit */; }; 4BC21A2F27238B7500229F0E /* RunLoopExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */; }; 4BC6DD1C2A60E6AD001EC129 /* ReportBrokenSiteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC6DD1B2A60E6AD001EC129 /* ReportBrokenSiteView.swift */; }; 4BE2756827304F57006B20B0 /* URLRequestExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE27566272F878F006B20B0 /* URLRequestExtension.swift */; }; @@ -1282,6 +1287,7 @@ 37FCAACB2993149A000E420A /* Waitlist */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Waitlist; sourceTree = ""; }; 37FD780E2A29E28B00B36DB1 /* SyncErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorHandler.swift; sourceTree = ""; }; 4B0295182537BC6700E00CEF /* ConfigurationDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationDebugViewController.swift; sourceTree = ""; }; + 4B274F5F2AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionWidgetRefreshModel.swift; sourceTree = ""; }; 4B470ED5299C49800086EBDC /* AppTrackingProtectionDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionDatabase.swift; sourceTree = ""; }; 4B470ED8299C4AED0086EBDC /* AppTrackingProtectionModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AppTrackingProtectionModel.xcdatamodel; sourceTree = ""; }; 4B470EDA299C4FB20086EBDC /* AppTrackingProtectionListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionListViewModel.swift; sourceTree = ""; }; @@ -1289,6 +1295,7 @@ 4B470EE2299C6DD10086EBDC /* AppTrackingProtectionStoringModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionStoringModel.swift; sourceTree = ""; }; 4B52648A25F9613B00CB4C24 /* trackerData.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = trackerData.json; sourceTree = ""; }; 4B53648926718D0E001AA041 /* EmailWaitlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailWaitlist.swift; sourceTree = ""; }; + 4B5C46292AF2A6E6002A4432 /* VPNIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNIntents.swift; sourceTree = ""; }; 4B60AC96252EC07B00E8D219 /* fullscreenvideo.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = fullscreenvideo.js; sourceTree = ""; }; 4B60ACA0252EC0B100E8D219 /* FullScreenVideoUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenVideoUserScript.swift; sourceTree = ""; }; 4B62C4B925B930DD008912C6 /* AppConfigurationFetchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationFetchTests.swift; sourceTree = ""; }; @@ -1303,6 +1310,7 @@ 4B83397029AC18C9003F7EA9 /* AppTrackingProtectionStoringModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionStoringModelTests.swift; sourceTree = ""; }; 4B83397229AFB8D2003F7EA9 /* AppTrackingProtectionFeedbackModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionFeedbackModel.swift; sourceTree = ""; }; 4B83397429AFBCE6003F7EA9 /* AppTrackingProtectionFeedbackModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTrackingProtectionFeedbackModelTests.swift; sourceTree = ""; }; + 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWidget.swift; sourceTree = ""; }; 4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunLoopExtensionTests.swift; sourceTree = ""; }; 4BC6DD1B2A60E6AD001EC129 /* ReportBrokenSiteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportBrokenSiteView.swift; sourceTree = ""; }; 4BE27566272F878F006B20B0 /* URLRequestExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = URLRequestExtension.swift; path = ../DuckDuckGo/URLRequestExtension.swift; sourceTree = ""; }; @@ -2586,6 +2594,7 @@ 8512EA5124ED30D20073EE19 /* SwiftUI.framework in Frameworks */, 85DF714624F7FE6100C89288 /* Core.framework in Frameworks */, 8512EA4F24ED30D20073EE19 /* WidgetKit.framework in Frameworks */, + 4BBBBA872B02E85400D965DA /* DesignResourcesKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3379,6 +3388,14 @@ name = WindowsBrowser; sourceTree = ""; }; + 4B274F5E2AFEAEB3003F0745 /* Widget */ = { + isa = PBXGroup; + children = ( + 4B274F5F2AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift */, + ); + name = Widget; + sourceTree = ""; + }; 4B470ED4299C484B0086EBDC /* AppTrackingProtection */ = { isa = PBXGroup; children = ( @@ -3394,6 +3411,14 @@ name = AppTrackingProtection; sourceTree = ""; }; + 4B5C46282AF2A6DB002A4432 /* Intents */ = { + isa = PBXGroup; + children = ( + 4B5C46292AF2A6E6002A4432 /* VPNIntents.swift */, + ); + name = Intents; + sourceTree = ""; + }; 4B6484F427FD1E390050A7A1 /* Waitlist */ = { isa = PBXGroup; children = ( @@ -3701,6 +3726,7 @@ 8512EA5324ED30D20073EE19 /* Widgets.swift */, 853273AF24FEFE4600E3C778 /* WidgetsExtension.entitlements */, 853273A924FEF24300E3C778 /* WidgetViews.swift */, + 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */, ); path = Widgets; sourceTree = ""; @@ -4496,6 +4522,8 @@ EE0153DF2A6EABAF002A8B26 /* Helpers */, EEFD562D2A65B68B00DAEC48 /* Invite */, EECD94B32A28B96C0085C66E /* Status */, + 4B5C46282AF2A6DB002A4432 /* Intents */, + 4B274F5E2AFEAEB3003F0745 /* Widget */, EE8594982A44791C008A6D06 /* NetworkProtectionTunnelController.swift */, ); name = NetworkProtection; @@ -5417,6 +5445,9 @@ 85DF714924F7FE6100C89288 /* PBXTargetDependency */, ); name = WidgetsExtension; + packageProductDependencies = ( + 4BBBBA862B02E85400D965DA /* DesignResourcesKit */, + ); productName = WidgetsExtension; productReference = 8512EA4D24ED30D20073EE19 /* WidgetsExtension.appex */; productType = "com.apple.product-type.app-extension"; @@ -6379,6 +6410,7 @@ 85C861E628FF1B5F00189466 /* HomeViewSectionRenderersExtension.swift in Sources */, F1D477C61F2126CC0031ED49 /* OmniBarState.swift in Sources */, 85F2FFCD2211F615006BB258 /* MainViewController+KeyCommands.swift in Sources */, + 4B274F602AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift in Sources */, 0268FC132A449F04000EE6A2 /* OnboardingContainerView.swift in Sources */, 858650D9246B0D3C00C36F8A /* DaxOnboardingViewController.swift in Sources */, 312E5746283BB04A00C18FA0 /* AutofillEmptySearchView.swift in Sources */, @@ -6437,6 +6469,7 @@ 020108A729A6ABF600644F9D /* AppTPToggleView.swift in Sources */, 02A54A982A093126000C8FED /* AppTPHomeViewModel.swift in Sources */, F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */, + 4B5C462A2AF2A6E6002A4432 /* VPNIntents.swift in Sources */, 310742A62848CD780012660B /* BackForwardMenuHistoryItem.swift in Sources */, 858566FB252E55D6007501B8 /* ImageCacheDebugViewController.swift in Sources */, 0290472E29E99A2F0008FE3C /* GenericIconView.swift in Sources */, @@ -6667,6 +6700,8 @@ 853273B324FF114700E3C778 /* DeepLinks.swift in Sources */, 853273B424FFB36100E3C778 /* UIColorExtension.swift in Sources */, 853273AB24FEF27500E3C778 /* WidgetViews.swift in Sources */, + 4B5C462B2AF2BDC4002A4432 /* VPNIntents.swift in Sources */, + 4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */, 8512EA5424ED30D20073EE19 /* Widgets.swift in Sources */, 85DB12EB2A1FE2A4000A4A72 /* LockScreenWidgets.swift in Sources */, 8544C37C250B827300A0FE73 /* UserText.swift in Sources */, @@ -8499,7 +8534,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG NETWORK_PROTECTION"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG NETWORK_PROTECTION ALPHA"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = "$(ARCHS_STANDARD_64_BIT)"; @@ -9197,6 +9232,11 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Persistence; }; + 4BBBBA862B02E85400D965DA /* DesignResourcesKit */ = { + isa = XCSwiftPackageProductDependency; + package = F42D541B29DCA40B004C4FF1 /* XCRemoteSwiftPackageReference "DesignResourcesKit" */; + productName = DesignResourcesKit; + }; 851481872A600EFC00ABC65F /* RemoteMessaging */ = { isa = XCSwiftPackageProductDependency; package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo/AppDelegate+AppDeepLinks.swift b/DuckDuckGo/AppDelegate+AppDeepLinks.swift index 60788a6abc..ec8d49dbac 100644 --- a/DuckDuckGo/AppDelegate+AppDeepLinks.swift +++ b/DuckDuckGo/AppDelegate+AppDeepLinks.swift @@ -22,6 +22,7 @@ import Core extension AppDelegate { + // swiftlint:disable:next cyclomatic_complexity func handleAppDeepLink(_ app: UIApplication, _ mainViewController: MainViewController?, _ url: URL) -> Bool { guard let mainViewController else { return false } @@ -50,6 +51,11 @@ extension AppDelegate { case .newEmail: mainViewController.newEmailAddress() + case .openVPN: +#if NETWORK_PROTECTION + presentNetworkProtectionStatusSettingsModal() +#endif + default: guard app.applicationState == .active, let currentTab = mainViewController.currentTab else { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 9b3a32e512..3732a3fd5e 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -67,6 +67,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private var appTrackingProtectionDatabase: CoreDataDatabase = AppTrackingProtectionDatabase.make() #endif +#if NETWORK_PROTECTION + private let widgetRefreshModel = NetworkProtectionWidgetRefreshModel() +#endif + private var autoClear: AutoClear? private var showKeyboardIfSettingOn = true private var lastBackgroundDate: Date? @@ -278,6 +282,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { syncDataProviders: syncDataProviders, appSettings: AppDependencyProvider.shared.appSettings) #endif + main.loadViewIfNeeded() window = UIWindow(frame: UIScreen.main.bounds) @@ -316,6 +321,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { AppDependencyProvider.shared.appSettings.setAutofillIsNewInstallForOnByDefault() } +#if NETWORK_PROTECTION + widgetRefreshModel.beginObservingVPNStatus() +#endif + return true } @@ -411,6 +420,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { syncService.scheduler.notifyAppLifecycleEvent() fireFailedCompilationsPixelIfNeeded() refreshShortcuts() + +#if NETWORK_PROTECTION + widgetRefreshModel.refreshVPNWidget() +#endif } func applicationWillResignActive(_ application: UIApplication) { @@ -566,7 +579,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } NotificationCenter.default.post(name: AutofillLoginListAuthenticator.Notifications.invalidateContext, object: nil) - mainViewController?.clearNavigationStack() + + // The openVPN action handles the navigation stack on its own and does not need it to be cleared + if url != AppDeepLinkSchemes.openVPN.url { + mainViewController?.clearNavigationStack() + } + autoClear?.applicationWillMoveToForeground() showKeyboardIfSettingOn = false @@ -814,7 +832,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { } #if NETWORK_PROTECTION - private func presentNetworkProtectionStatusSettingsModal() { + func presentNetworkProtectionStatusSettingsModal() { if #available(iOS 15, *) { let networkProtectionRoot = NetworkProtectionRootViewController() presentSettings(with: networkProtectionRoot) diff --git a/DuckDuckGo/NetworkProtectionStatusViewModel.swift b/DuckDuckGo/NetworkProtectionStatusViewModel.swift index dc8924c38b..6d6a817072 100644 --- a/DuckDuckGo/NetworkProtectionStatusViewModel.swift +++ b/DuckDuckGo/NetworkProtectionStatusViewModel.swift @@ -22,6 +22,7 @@ import Foundation import Combine import NetworkProtection +import WidgetKit final class NetworkProtectionStatusViewModel: ObservableObject { private static var dateFormatter: DateComponentsFormatter = { @@ -178,6 +179,8 @@ final class NetworkProtectionStatusViewModel: ObservableObject { } else { await disableNetP() } + + WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") } @MainActor diff --git a/DuckDuckGo/NetworkProtectionWidgetRefreshModel.swift b/DuckDuckGo/NetworkProtectionWidgetRefreshModel.swift new file mode 100644 index 0000000000..b9b5871ffc --- /dev/null +++ b/DuckDuckGo/NetworkProtectionWidgetRefreshModel.swift @@ -0,0 +1,46 @@ +// +// NetworkProtectionWidgetRefreshModel.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NETWORK_PROTECTION + +import Foundation +import Combine +import NetworkExtension +import WidgetKit + +class NetworkProtectionWidgetRefreshModel { + + private var cancellable: AnyCancellable? + + public func beginObservingVPNStatus() { + cancellable = NotificationCenter.default.publisher(for: .NEVPNStatusDidChange) + .debounce(for: .seconds(0.5), scheduler: RunLoop.main) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.refreshVPNWidget() + } + } + + public func refreshVPNWidget() { + WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") + } + +} + +#endif diff --git a/DuckDuckGo/VPNIntents.swift b/DuckDuckGo/VPNIntents.swift new file mode 100644 index 0000000000..716396607f --- /dev/null +++ b/DuckDuckGo/VPNIntents.swift @@ -0,0 +1,102 @@ +// +// VPNIntents.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppIntents +import NetworkExtension +import WidgetKit + +@available(iOS 17.0, *) +struct DisableVPNIntent: AppIntent { + + static let title: LocalizedStringResource = "Disable VPN" + static let description: LocalizedStringResource = "Disables the DuckDuckGo VPN" + static let openAppWhenRun: Bool = false + + @MainActor + func perform() async throws -> some IntentResult { + do { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + guard let manager = managers.first else { + return .result() + } + + manager.isOnDemandEnabled = false + try await manager.saveToPreferences() + manager.connection.stopVPNTunnel() + + WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") + var iterations = 0 + + while iterations <= 10 { + try? await Task.sleep(interval: .seconds(0.5)) + + if manager.connection.status == .disconnected { + return .result() + } + + iterations += 1 + } + + return .result() + } catch { + return .result() + } + } + +} + +@available(iOS 17.0, *) +struct EnableVPNIntent: AppIntent { + + static let title: LocalizedStringResource = "Enable VPN" + static let description: LocalizedStringResource = "Enables the DuckDuckGo VPN" + static let openAppWhenRun: Bool = false + + @MainActor + func perform() async throws -> some IntentResult { + do { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + guard let manager = managers.first else { + return .result() + } + + manager.isOnDemandEnabled = true + try await manager.saveToPreferences() + try manager.connection.startVPNTunnel() + + WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") + var iterations = 0 + + while iterations <= 10 { + try? await Task.sleep(interval: .seconds(0.5)) + + if manager.connection.status == .connected { + return .result() + } + + iterations += 1 + } + + return .result() + } catch { + return .result() + } + } + +} diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 5db8d2c3c5..d3ca118411 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -20,15 +20,18 @@ #if NETWORK_PROTECTION import Foundation -import NetworkProtection import Common +import Combine import Core import Networking import NetworkExtension +import NetworkProtection // Initial implementation for initial Network Protection tests. Will be fleshed out with https://app.asana.com/0/1203137811378537/1204630829332227/f final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { + private var cancellables = Set() + // MARK: - PacketTunnelProvider.Event reporting private static var packetTunnelProviderEvents: EventMapping = .init { event, _, _, _ in @@ -183,6 +186,7 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { debugEvents: Self.networkProtectionDebugEvents(controllerErrorStore: errorStore), providerEvents: Self.packetTunnelProviderEvents) startMonitoringMemoryPressureEvents() + observeServerChanges() APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) } @@ -209,6 +213,14 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { source.resume() } } + + private func observeServerChanges() { + lastSelectedServerInfoPublisher.sink { server in + let location = server?.serverLocation ?? "Unknown Location" + UserDefaults.networkProtectionGroupDefaults.set(location, forKey: NetworkProtectionUserDefaultKeys.lastSelectedServer) + } + .store(in: &cancellables) + } } #endif diff --git a/Widgets/Assets.xcassets/vpn-off.imageset/Contents.json b/Widgets/Assets.xcassets/vpn-off.imageset/Contents.json new file mode 100644 index 0000000000..839c6e2214 --- /dev/null +++ b/Widgets/Assets.xcassets/vpn-off.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "vpn-off.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Widgets/Assets.xcassets/vpn-off.imageset/vpn-off.pdf b/Widgets/Assets.xcassets/vpn-off.imageset/vpn-off.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8438e0ef8e27b1cdb6d0e04e3cd1fa039064acd1 GIT binary patch literal 8715 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 literal 0 HcmV?d00001 diff --git a/Widgets/Assets.xcassets/vpn-on.imageset/Contents.json b/Widgets/Assets.xcassets/vpn-on.imageset/Contents.json new file mode 100644 index 0000000000..ec33d453f4 --- /dev/null +++ b/Widgets/Assets.xcassets/vpn-on.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "vpn-on.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Widgets/Assets.xcassets/vpn-on.imageset/vpn-on.pdf b/Widgets/Assets.xcassets/vpn-on.imageset/vpn-on.pdf new file mode 100644 index 0000000000000000000000000000000000000000..09c2cae8055b1684d588daff48cae65a0ebc1dac GIT binary patch literal 9104 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 VPNStatusTimelineEntry { + return VPNStatusTimelineEntry(date: Date(), status: .status(.connected), location: "Los Angeles, CA") + } + + func getSnapshot(in context: Context, completion: @escaping (VPNStatusTimelineEntry) -> Void) { + let entry = VPNStatusTimelineEntry(date: Date(), status: .status(.connected), location: "Los Angeles, CA") + completion(entry) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + NETunnelProviderManager.loadAllFromPreferences { managers, error in + let defaults = UserDefaults.networkProtectionGroupDefaults + let location = defaults.string(forKey: NetworkProtectionUserDefaultKeys.lastSelectedServer) ?? "Unknown Location" + let expiration = Date().addingTimeInterval(TimeInterval.minutes(5)) + + if error != nil { + let entry = VPNStatusTimelineEntry(date: expiration, status: .error, location: location) + let timeline = Timeline(entries: [entry], policy: .atEnd) + completion(timeline) + return + } + + guard let manager = managers?.first else { + let entry = VPNStatusTimelineEntry(date: expiration, status: .notConfigured, location: location) + let timeline = Timeline(entries: [entry], policy: .atEnd) + completion(timeline) + return + } + + let status = manager.connection.status + let entry = VPNStatusTimelineEntry(date: expiration, status: .status(status), location: location) + let timeline = Timeline(entries: [entry], policy: .atEnd) + + completion(timeline) + } + } +} + +extension NEVPNStatus { + var description: String { + switch self { + case .connected: return "Connected" + case .connecting: return "Connecting" + case .disconnected: return "Disconnected" + case .disconnecting: return "Disconnecting" + case .invalid: return "Invalid" + case .reasserting: return "Reasserting" + default: return "Unknown Status" + } + } + + var isConnected: Bool { + switch self { + case .connected, .connecting, .reasserting: return true + case .disconnecting, .disconnected: return false + default: return false + } + } +} + +@available(iOSApplicationExtension 17.0, *) +struct VPNStatusView: View { + @Environment(\.widgetFamily) var family: WidgetFamily + var entry: VPNStatusTimelineProvider.Entry + + @ViewBuilder + var body: some View { + Group { + switch entry.status { + case .status(let status): + HStack { + connectionView(with: status) + .padding([.leading, .trailing], 16) + + Spacer() + } + case .error: + Text("Error") + .foregroundStyle(Color.black) + case .notConfigured: + Text("VPN Not Configured") + .foregroundStyle(Color.black) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .containerBackground(for: .widget) { + switch entry.status { + case .status(let status): + switch status { + case .connecting, .connected, .reasserting: + Color.vpnWidgetBackgroundColor + case .disconnecting, .disconnected, .invalid: + Color.white + @unknown default: + Color.white + } + case .error, .notConfigured: + Color.white + } + } + } + + private func connectionView(with status: NEVPNStatus) -> some View { + HStack { + VStack(alignment: .leading, spacing: 0) { + Image(headerImageName(with: status)) + .frame(width: 50, height: 54) + .padding(.top, 15) + + Spacer() + + Text(title(with: status)) + .font(.system(size: 16, weight: .semibold)) + .fontWeight(.semibold) + .foregroundStyle(status.isConnected ? Color.white : Color.black) + + Text(status.isConnected ? entry.location : "VPN is Off") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(status.isConnected ? Color.white : Color.black) + .opacity(status.isConnected ? 0.8 : 0.6) + + switch status { + case .connected, .connecting, .reasserting: + Button(intent: DisableVPNIntent()) { + Text("Disconnect") + .font(.system(size: 15, weight: .medium)) + .fontWeight(.semibold) + } + .foregroundStyle(Color.vpnWidgetBackgroundColor) + .buttonStyle(.borderedProminent) + .tint(.white) + .disabled(status != .connected) + .padding(.top, 6) + .padding(.bottom, 16) + case .disconnected, .disconnecting: + Button(intent: EnableVPNIntent()) { + Text("Connect") + .font(.system(size: 15, weight: .medium)) + .fontWeight(.semibold) + } + .foregroundStyle(.white) + .buttonStyle(.borderedProminent) + .tint(Color.vpnWidgetBackgroundColor) + .disabled(status != .disconnected) + .padding(.top, 6) + .padding(.bottom, 16) + default: + Spacer() + } + } + } + } + + private func headerImageName(with status: NEVPNStatus) -> String { + switch status { + case .connecting, .connected, .reasserting: return "vpn-on" + case .disconnecting, .disconnected: return "vpn-off" + case .invalid: return "vpn-off" + @unknown default: return "vpn-off" + } + } + + private func title(with status: NEVPNStatus) -> String { + switch status { + case .connecting, .connected, .reasserting: return "Protected" + case .disconnecting, .disconnected: return "Unprotected" + case .invalid: return "Invalid" + @unknown default: return "Unknown" + } + } + +} + +@available(iOSApplicationExtension 17.0, *) +struct VPNStatusWidget: Widget { + let kind: String = "VPNStatusWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: VPNStatusTimelineProvider()) { entry in + VPNStatusView(entry: entry).widgetURL(DeepLinks.openVPN) + } + .configurationDisplayName("VPN Status") + .description("View and manage the VPN connection") + .supportedFamilies([.systemSmall]) + .contentMarginsDisabled() + } +} + +struct VPNStatusView_Previews: PreviewProvider { + + static let connectedState = VPNStatusTimelineProvider.Entry( + date: Date(), + status: .status(.connected), + location: "Paoli, PA" + ) + + static let disconnectedState = VPNStatusTimelineProvider.Entry( + date: Date(), + status: .status(.disconnected), + location: "Paoli, PA" + ) + + static let notConfiguredState = VPNStatusTimelineProvider.Entry( + date: Date(), + status: .notConfigured, + location: "Paoli, PA" + ) + + static var previews: some View { + if #available(iOSApplicationExtension 17.0, *) { + VPNStatusView(entry: connectedState) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .environment(\.colorScheme, .light) + + VPNStatusView(entry: connectedState) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .environment(\.colorScheme, .dark) + + VPNStatusView(entry: disconnectedState) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .environment(\.colorScheme, .light) + + VPNStatusView(entry: disconnectedState) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .environment(\.colorScheme, .dark) + + VPNStatusView(entry: notConfiguredState) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .environment(\.colorScheme, .light) + + VPNStatusView(entry: notConfiguredState) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .environment(\.colorScheme, .dark) + } else { + Text("iOS 17 required") + } + } + +} + +extension Color { + + static var vpnWidgetBackgroundColor: Color { + let color = UIColor(designSystemColor: .accent).resolvedColor(with: UITraitCollection(userInterfaceStyle: .light)) + return Color(color) + } + +} + +#endif diff --git a/Widgets/Widgets.swift b/Widgets/Widgets.swift index ae5ae2b1d2..39ab7b0057 100644 --- a/Widgets/Widgets.swift +++ b/Widgets/Widgets.swift @@ -25,6 +25,7 @@ import CoreData import Kingfisher import Bookmarks import Persistence +import NetworkExtension struct Favorite { @@ -202,6 +203,12 @@ struct Widgets: WidgetBundle { SearchWidget() FavoritesWidget() +#if ALPHA + if #available(iOSApplicationExtension 17.0, *) { + VPNStatusWidget() + } +#endif + if #available(iOSApplicationExtension 16.0, *) { SearchLockScreenWidget() VoiceSearchLockScreenWidget() diff --git a/Widgets/WidgetsExtension.entitlements b/Widgets/WidgetsExtension.entitlements index 2dd2c82001..4d39b3e562 100644 --- a/Widgets/WidgetsExtension.entitlements +++ b/Widgets/WidgetsExtension.entitlements @@ -6,6 +6,7 @@ $(GROUP_ID_PREFIX).bookmarks $(GROUP_ID_PREFIX).database + $(GROUP_ID_PREFIX).netp diff --git a/WidgetsExtensionAlpha.entitlements b/WidgetsExtensionAlpha.entitlements index d5324a15bd..4d39b3e562 100644 --- a/WidgetsExtensionAlpha.entitlements +++ b/WidgetsExtensionAlpha.entitlements @@ -4,8 +4,9 @@ com.apple.security.application-groups - group.com.duckduckgo.alpha.database - group.com.duckduckgo.alpha.bookmarks + $(GROUP_ID_PREFIX).bookmarks + $(GROUP_ID_PREFIX).database + $(GROUP_ID_PREFIX).netp