From 9ced0914a2b93b6ada1df9282a5946e2638d48e4 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Sun, 6 Oct 2024 17:36:52 +0200 Subject: [PATCH 1/8] Adds Siri support for some basic VPN commands --- DuckDuckGo.xcodeproj/project.pbxproj | 27 ++++++++++------ DuckDuckGo/DuckDuckGo.entitlements | 2 ++ DuckDuckGo/DuckDuckGoAlpha.entitlements | 2 ++ DuckDuckGo/VPNAutoShortcuts.swift | 40 +++++++++++++++++++++++ DuckDuckGo/VPNIntents.swift | 42 ++++++++++++++++++++++--- DuckDuckGo/VPNToggleIntent.swift | 1 + 6 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 DuckDuckGo/VPNAutoShortcuts.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 550d625a23..54e262136b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -363,6 +363,8 @@ 7B4DC5C22CB2AE4600EE5CC2 /* WidgetKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */; }; 7B4DC5C32CB2AF0700EE5CC2 /* WidgetKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */; }; 7B4DC5C42CB2B1D000EE5CC2 /* WidgetKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */; }; + 7B4DC5E12CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */; }; + 7B4DC5E22CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */; }; 7BC571202BDBB877003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; 7BFC32B02CB291BB007A8E17 /* VPNControlWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFC32AF2CB291BB007A8E17 /* VPNControlWidget.swift */; }; @@ -1273,6 +1275,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 7B4DC5D92CB2D0F100EE5CC2 /* Embed ExtensionKit Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(EXTENSIONS_FOLDER_PATH)"; + dstSubfolderSpec = 16; + files = ( + ); + name = "Embed ExtensionKit Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; 83E282AC20BC1840005FBE88 /* Embed App Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -1648,6 +1660,7 @@ 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNToggleIntent.swift; sourceTree = ""; }; 7B4DC5BF2CB2A4A500EE5CC2 /* VPNStatusValueProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatusValueProvider.swift; sourceTree = ""; }; 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetKind.swift; sourceTree = ""; }; + 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNAutoShortcuts.swift; sourceTree = ""; }; 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNActivationDateStore.swift; sourceTree = ""; }; 7BFC32AF2CB291BB007A8E17 /* VPNControlWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNControlWidget.swift; sourceTree = ""; }; 83004E7F2193BB8200DA013C /* WKNavigationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKNavigationExtension.swift; sourceTree = ""; }; @@ -3748,6 +3761,7 @@ children = ( 4B5C46292AF2A6E6002A4432 /* VPNIntents.swift */, 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */, + 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */, ); name = Intents; sourceTree = ""; @@ -6512,6 +6526,7 @@ F10307651E7D5B2C0059FEC7 /* Copy Frameworks */, 83E282AC20BC1840005FBE88 /* Embed App Extensions */, EE9286812A812BD2002B7818 /* Embed PacketTunnelProvider */, + 7B4DC5D92CB2D0F100EE5CC2 /* Embed ExtensionKit Extensions */, ); buildRules = ( ); @@ -6752,7 +6767,7 @@ 84E3418A1E2F7EFB00BDBA6F /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1420; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1500; ORGANIZATIONNAME = DuckDuckGo; TargetAttributes = { @@ -7458,6 +7473,7 @@ C185ED612BD4329700BAE9DC /* ImportPasswordsStatusHandler.swift in Sources */, CB9B8739278C8E72001F4906 /* WidgetEducationViewController.swift in Sources */, F4D9C4FA25117A0F00814B71 /* HomeMessageStorage.swift in Sources */, + 7B4DC5E22CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift in Sources */, D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */, D664C7CC2B289AA200CBFA76 /* SubscriptionPagesUserScript.swift in Sources */, AA3D854523D9942200788410 /* AppIconSettingsViewController.swift in Sources */, @@ -8121,6 +8137,7 @@ 853273AB24FEF27500E3C778 /* WidgetViews.swift in Sources */, 4B5C462B2AF2BDC4002A4432 /* VPNIntents.swift in Sources */, 4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */, + 7B4DC5E12CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift in Sources */, 8512EA5424ED30D20073EE19 /* Widgets.swift in Sources */, 85DB12EB2A1FE2A4000A4A72 /* LockScreenWidgets.swift in Sources */, 4BD96E0E2C4DCFD7003BC32C /* VPNSnoozeLiveActivityWidget.swift in Sources */, @@ -9470,7 +9487,6 @@ 84E341BB1E2F7EFC00BDBA6F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; @@ -9495,7 +9511,6 @@ 84E341BC1E2F7EFC00BDBA6F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; @@ -9518,7 +9533,6 @@ 84E341BE1E2F7EFC00BDBA6F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGoTests/Info.plist; @@ -9538,7 +9552,6 @@ 84E341BF1E2F7EFC00BDBA6F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGoTests/Info.plist; @@ -9971,7 +9984,6 @@ D664C7DF2B28A0FD00CBFA76 /* Alpha Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = "DDG-AppIcon-Alpha"; CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; @@ -10199,7 +10211,6 @@ D664C7E72B28A0FD00CBFA76 /* Alpha Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGoTests/Info.plist; @@ -10363,7 +10374,6 @@ EE5A7C472A82BBB700387C84 /* Alpha */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = "DDG-AppIcon-Alpha"; CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; @@ -10602,7 +10612,6 @@ EE5A7C4F2A82BBB700387C84 /* Alpha */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGoTests/Info.plist; diff --git a/DuckDuckGo/DuckDuckGo.entitlements b/DuckDuckGo/DuckDuckGo.entitlements index cfeb809f1b..5e8a1b0e85 100644 --- a/DuckDuckGo/DuckDuckGo.entitlements +++ b/DuckDuckGo/DuckDuckGo.entitlements @@ -8,6 +8,8 @@ packet-tunnel-provider + com.apple.developer.siri + com.apple.developer.web-browser com.apple.security.application-groups diff --git a/DuckDuckGo/DuckDuckGoAlpha.entitlements b/DuckDuckGo/DuckDuckGoAlpha.entitlements index b8debe8f31..eeafb7e9bb 100644 --- a/DuckDuckGo/DuckDuckGoAlpha.entitlements +++ b/DuckDuckGo/DuckDuckGoAlpha.entitlements @@ -6,6 +6,8 @@ packet-tunnel-provider + com.apple.developer.siri + com.apple.developer.web-browser com.apple.security.application-groups diff --git a/DuckDuckGo/VPNAutoShortcuts.swift b/DuckDuckGo/VPNAutoShortcuts.swift new file mode 100644 index 0000000000..5538dd3c23 --- /dev/null +++ b/DuckDuckGo/VPNAutoShortcuts.swift @@ -0,0 +1,40 @@ +// +// VPNAutoShortcuts.swift +// DuckDuckGo +// +// 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 AppIntents +import Foundation + +@available(iOS 17.0, *) +struct VPNAutoShortcutsiOS17: AppShortcutsProvider { + + @AppShortcutsBuilder + static var appShortcuts: [AppShortcut] { + AppShortcut(intent: EnableVPNIntent(), + phrases: EnableVPNIntent.phrases, + systemImageName: "globe") + AppShortcut(intent: DisableVPNIntent(), + phrases: DisableVPNIntent.phrases, + systemImageName: "globe") + AppShortcut(intent: VPNToggleIntent(), + phrases: ["Toggle \(.applicationName) VPN", + "Toggle a VPN connection with \(.applicationName)", + "Switch \(.applicationName) VPN on/off"], + systemImageName: "globe") + } +} diff --git a/DuckDuckGo/VPNIntents.swift b/DuckDuckGo/VPNIntents.swift index e064b99765..0ec9d477c6 100644 --- a/DuckDuckGo/VPNIntents.swift +++ b/DuckDuckGo/VPNIntents.swift @@ -28,10 +28,26 @@ import Core @available(iOS 17.0, *) struct DisableVPNIntent: AppIntent { - static let title: LocalizedStringResource = "Disable VPN" + static let title: LocalizedStringResource = "Disable DuckDuckGo VPN" static let description: LocalizedStringResource = "Disables the DuckDuckGo VPN" static let openAppWhenRun: Bool = false - static let isDiscoverable: Bool = false + static let isDiscoverable: Bool = true + + static var phrases: [AppShortcutPhrase] { + [ + "Turn off VPN with \(.applicationName)", + "Turn the VPN off with \(.applicationName)", + "Turn off \(.applicationName) VPN", + "Turn off the \(.applicationName) VPN", + "Disable VPN with \(.applicationName)", + "Disable the VPN with \(.applicationName)", + "Disable \(.applicationName) VPN", + "Disable the \(.applicationName) VPN", + "Stop \(.applicationName) VPN", + "Stop the \(.applicationName) VPN", + "Stop a VPN connection with \(.applicationName)" + ] + } @MainActor func perform() async throws -> some IntentResult { @@ -75,10 +91,28 @@ struct DisableVPNIntent: AppIntent { @available(iOS 17.0, *) struct EnableVPNIntent: AppIntent { - static let title: LocalizedStringResource = "Enable VPN" + static let title: LocalizedStringResource = "Enable DuckDuckGo VPN" static let description: LocalizedStringResource = "Enables the DuckDuckGo VPN" static let openAppWhenRun: Bool = false - static let isDiscoverable: Bool = false + static let isDiscoverable: Bool = true + + static var phrases: [AppShortcutPhrase] { + [ + "Turn on VPN with \(.applicationName)", + "Turn {the} VPN on with \(.applicationName)", + "Turn on \(.applicationName) VPN", + "Turn on the \(.applicationName) VPN", + "Enable VPN with \(.applicationName)", + "Enable the VPN with \(.applicationName)", + "Enable \(.applicationName) VPN", + "Enable the \(.applicationName) VPN", + "Start \(.applicationName) VPN", + "Start the \(.applicationName) VPN", + "Start the VPN connection with \(.applicationName)", + "Secure my connection with \(.applicationName)", + "Protect my connection with \(.applicationName)" + ] + } @MainActor func perform() async throws -> some IntentResult { diff --git a/DuckDuckGo/VPNToggleIntent.swift b/DuckDuckGo/VPNToggleIntent.swift index c3b15f4a52..f49c282ee3 100644 --- a/DuckDuckGo/VPNToggleIntent.swift +++ b/DuckDuckGo/VPNToggleIntent.swift @@ -29,6 +29,7 @@ import Core struct VPNToggleIntent: SetValueIntent { static let title: LocalizedStringResource = "Toggle DuckDuckGo VPN" static let description: LocalizedStringResource = "Toggles the DuckDuckGo VPN" + static let isDiscoverable: Bool = false @Parameter(title: "Enabled") var value: Bool From 8eb15bf182ca4f719fc7047d62ff8ced963c093b Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Sun, 6 Oct 2024 19:54:36 +0200 Subject: [PATCH 2/8] Makes some changes to fix some issues with Siri VPN commands --- DuckDuckGo.xcodeproj/project.pbxproj | 2 -- DuckDuckGo/VPNAutoShortcuts.swift | 35 ++++++++++++++++++++------ DuckDuckGo/VPNIntents.swift | 37 ++-------------------------- 3 files changed, 30 insertions(+), 44 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 54e262136b..74a2ba5198 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -363,7 +363,6 @@ 7B4DC5C22CB2AE4600EE5CC2 /* WidgetKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */; }; 7B4DC5C32CB2AF0700EE5CC2 /* WidgetKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */; }; 7B4DC5C42CB2B1D000EE5CC2 /* WidgetKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */; }; - 7B4DC5E12CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */; }; 7B4DC5E22CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */; }; 7BC571202BDBB877003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; @@ -8137,7 +8136,6 @@ 853273AB24FEF27500E3C778 /* WidgetViews.swift in Sources */, 4B5C462B2AF2BDC4002A4432 /* VPNIntents.swift in Sources */, 4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */, - 7B4DC5E12CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift in Sources */, 8512EA5424ED30D20073EE19 /* Widgets.swift in Sources */, 85DB12EB2A1FE2A4000A4A72 /* LockScreenWidgets.swift in Sources */, 4BD96E0E2C4DCFD7003BC32C /* VPNSnoozeLiveActivityWidget.swift in Sources */, diff --git a/DuckDuckGo/VPNAutoShortcuts.swift b/DuckDuckGo/VPNAutoShortcuts.swift index 5538dd3c23..b130f63dda 100644 --- a/DuckDuckGo/VPNAutoShortcuts.swift +++ b/DuckDuckGo/VPNAutoShortcuts.swift @@ -26,15 +26,36 @@ struct VPNAutoShortcutsiOS17: AppShortcutsProvider { @AppShortcutsBuilder static var appShortcuts: [AppShortcut] { AppShortcut(intent: EnableVPNIntent(), - phrases: EnableVPNIntent.phrases, + phrases: [ + "Connect \(.applicationName) VPN", + "Connect the \(.applicationName) VPN", + "Turn \(.applicationName) VPN on", + "Turn the \(.applicationName) VPN on", + "Turn on \(.applicationName) VPN", + "Turn on the \(.applicationName) VPN", + "Enable \(.applicationName) VPN", + "Enable the \(.applicationName) VPN", + "Start \(.applicationName) VPN", + "Start the \(.applicationName) VPN", + "Start the VPN connection with \(.applicationName)", + "Secure my connection with \(.applicationName)", + "Protect my connection with \(.applicationName)" + ], systemImageName: "globe") AppShortcut(intent: DisableVPNIntent(), - phrases: DisableVPNIntent.phrases, - systemImageName: "globe") - AppShortcut(intent: VPNToggleIntent(), - phrases: ["Toggle \(.applicationName) VPN", - "Toggle a VPN connection with \(.applicationName)", - "Switch \(.applicationName) VPN on/off"], + phrases: [ + "Disconnect \(.applicationName) VPN", + "Disconnect the \(.applicationName) VPN", + "Turn \(.applicationName) VPN off", + "Turn the \(.applicationName) VPN off", + "Turn off \(.applicationName) VPN", + "Turn off the \(.applicationName) VPN", + "Disable \(.applicationName) VPN", + "Disable the \(.applicationName) VPN", + "Stop \(.applicationName) VPN", + "Stop the \(.applicationName) VPN", + "Stop a VPN connection with \(.applicationName)" + ], systemImageName: "globe") } } diff --git a/DuckDuckGo/VPNIntents.swift b/DuckDuckGo/VPNIntents.swift index 0ec9d477c6..6e0c1f3036 100644 --- a/DuckDuckGo/VPNIntents.swift +++ b/DuckDuckGo/VPNIntents.swift @@ -32,22 +32,7 @@ struct DisableVPNIntent: AppIntent { static let description: LocalizedStringResource = "Disables the DuckDuckGo VPN" static let openAppWhenRun: Bool = false static let isDiscoverable: Bool = true - - static var phrases: [AppShortcutPhrase] { - [ - "Turn off VPN with \(.applicationName)", - "Turn the VPN off with \(.applicationName)", - "Turn off \(.applicationName) VPN", - "Turn off the \(.applicationName) VPN", - "Disable VPN with \(.applicationName)", - "Disable the VPN with \(.applicationName)", - "Disable \(.applicationName) VPN", - "Disable the \(.applicationName) VPN", - "Stop \(.applicationName) VPN", - "Stop the \(.applicationName) VPN", - "Stop a VPN connection with \(.applicationName)" - ] - } + static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication @MainActor func perform() async throws -> some IntentResult { @@ -85,7 +70,6 @@ struct DisableVPNIntent: AppIntent { return .result() } } - } @available(iOS 17.0, *) @@ -95,24 +79,7 @@ struct EnableVPNIntent: AppIntent { static let description: LocalizedStringResource = "Enables the DuckDuckGo VPN" static let openAppWhenRun: Bool = false static let isDiscoverable: Bool = true - - static var phrases: [AppShortcutPhrase] { - [ - "Turn on VPN with \(.applicationName)", - "Turn {the} VPN on with \(.applicationName)", - "Turn on \(.applicationName) VPN", - "Turn on the \(.applicationName) VPN", - "Enable VPN with \(.applicationName)", - "Enable the VPN with \(.applicationName)", - "Enable \(.applicationName) VPN", - "Enable the \(.applicationName) VPN", - "Start \(.applicationName) VPN", - "Start the \(.applicationName) VPN", - "Start the VPN connection with \(.applicationName)", - "Secure my connection with \(.applicationName)", - "Protect my connection with \(.applicationName)" - ] - } + static var authenticationPolicy: IntentAuthenticationPolicy = .alwaysAllowed @MainActor func perform() async throws -> some IntentResult { From 27aa4b17b813f6c979a3a3acec20d82ab8840058 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 10 Dec 2024 13:27:43 -0300 Subject: [PATCH 3/8] Improves pixels and VPN enable and disable intents --- Core/PixelEvent.swift | 8 ++++ DuckDuckGo.xcodeproj/project.pbxproj | 4 +- DuckDuckGo/Info.plist | 22 +++++----- DuckDuckGo/VPNIntents.swift | 65 +++++++++++++++------------- 4 files changed, 56 insertions(+), 43 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 5fc44638c9..dd3500c540 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -463,8 +463,12 @@ extension Pixel { case networkProtectionWidgetConnectAttempt case networkProtectionWidgetConnectSuccess + case networkProtectionWidgetConnectCancelled + case networkProtectionWidgetConnectFailure case networkProtectionWidgetDisconnectAttempt case networkProtectionWidgetDisconnectSuccess + case networkProtectionWidgetDisconnectCancelled + case networkProtectionWidgetDisconnectFailure case networkProtectionDNSUpdateCustom case networkProtectionDNSUpdateDefault @@ -1660,8 +1664,12 @@ extension Pixel.Event { case .networkProtectionWidgetConnectAttempt: return "m_netp_widget_connect_attempt" case .networkProtectionWidgetConnectSuccess: return "m_netp_widget_connect_success" + case .networkProtectionWidgetConnectCancelled: return "m_netp_widget_connect_cancelled" + case .networkProtectionWidgetConnectFailure: return "m_netp_widget_connect_failure" case .networkProtectionWidgetDisconnectAttempt: return "m_netp_widget_disconnect_attempt" case .networkProtectionWidgetDisconnectSuccess: return "m_netp_widget_disconnect_success" + case .networkProtectionWidgetDisconnectCancelled: return "m_netp_widget_disconnect_cancelled" + case .networkProtectionWidgetDisconnectFailure: return "m_netp_widget_disconnect_failure" // MARK: Secure Vault case .secureVaultL1KeyMigration: return "m_secure-vault_keystore_event_l1-key-migration" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 4e5d8625f0..74bcad4b1f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -235,7 +235,6 @@ 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 */; }; @@ -391,6 +390,7 @@ 7B4F87EC2D07396A0010B18F /* SiriEducation.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B4F87EB2D07396A0010B18F /* SiriEducation.xcassets */; }; 7B4F87EE2D0739EB0010B18F /* SiriBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4F87ED2D0739E80010B18F /* SiriBubbleView.swift */; }; 7B8E0EC62CC81B4900B2B722 /* TipKitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8E0EC52CC81B4800B2B722 /* TipKitController.swift */; }; + 7BC0BB982D08854400445624 /* VPNIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5C46292AF2A6E6002A4432 /* VPNIntents.swift */; }; 7BC571202BDBB877003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; 7BDBAD0E2CBFB3F1000379B7 /* VPN.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7BDBAD0D2CBFB3F1000379B7 /* VPN.xcassets */; }; @@ -8409,6 +8409,7 @@ files = ( 853273AE24FEF49600E3C778 /* ColorExtension.swift in Sources */, 4BD96E0F2C4DCFEB003BC32C /* VPNSnoozeActivityAttributes.swift in Sources */, + 7BC0BB982D08854400445624 /* VPNIntents.swift in Sources */, 373608932ABB432600629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */, 4BD96E102C4DF329003BC32C /* VPNSnoozeLiveActivityManager.swift in Sources */, 7BFC32B02CB291BB007A8E17 /* VPNControlWidget.swift in Sources */, @@ -8418,7 +8419,6 @@ 7B4DC5C02CB2A4A500EE5CC2 /* VPNStatusValueProvider.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 */, diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist index 6d3941b0ca..533d276e76 100644 --- a/DuckDuckGo/Info.plist +++ b/DuckDuckGo/Info.plist @@ -2,10 +2,6 @@ - LSApplicationQueriesSchemes - - youtube - BGTaskSchedulerPermittedIdentifiers com.duckduckgo.app.configurationRefresh @@ -168,10 +164,16 @@ $(GROUP_ID_PREFIX) ITSAppUsesNonExemptEncryption + LSApplicationQueriesSchemes + + youtube + LSRequiresIPhoneOS LSSupportsOpeningDocumentsInPlace + NSAdvertisingAttributionReportEndpoint + https://duckduckgo.com NSAppTransportSecurity NSAllowsArbitraryLoads @@ -189,10 +191,16 @@ Allows you to save images to your device NSSpeechRecognitionUsageDescription This is required to use voice search. DuckDuckGo never records what you say. + NSSupportsLiveActivities + NSUserActivityTypes + CancelSnoozeLiveActivityAppIntentIntent ConfigurationIntent + EnableVPNIntentIntent + SUBSCRIPTION_APP_GROUP + $(AppIdentifierPrefix)$(SUBSCRIPTION_APP_GROUP) UIApplicationShortcutItems @@ -227,10 +235,6 @@ UIStatusBarHidden - SUBSCRIPTION_APP_GROUP - $(AppIdentifierPrefix)$(SUBSCRIPTION_APP_GROUP) - NSSupportsLiveActivities - UIStatusBarStyle UIStatusBarStyleDefault UISupportedInterfaceOrientations~ipad @@ -240,8 +244,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSAdvertisingAttributionReportEndpoint - https://duckduckgo.com UIViewControllerBasedStatusBarAppearance diff --git a/DuckDuckGo/VPNIntents.swift b/DuckDuckGo/VPNIntents.swift index 6e0c1f3036..94f75e7c15 100644 --- a/DuckDuckGo/VPNIntents.swift +++ b/DuckDuckGo/VPNIntents.swift @@ -28,6 +28,10 @@ import Core @available(iOS 17.0, *) struct DisableVPNIntent: AppIntent { + private enum DisableAttemptFailure: CustomNSError { + case cancelled + } + static let title: LocalizedStringResource = "Disable DuckDuckGo VPN" static let description: LocalizedStringResource = "Disables the DuckDuckGo VPN" static let openAppWhenRun: Bool = false @@ -37,11 +41,11 @@ struct DisableVPNIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { do { - DailyPixel.fire(pixel: .networkProtectionWidgetDisconnectAttempt) + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectAttempt) let managers = try await NETunnelProviderManager.loadAllFromPreferences() guard let manager = managers.first else { - return .result() + throw DisableAttemptFailure.cancelled } manager.isOnDemandEnabled = false @@ -50,30 +54,34 @@ struct DisableVPNIntent: AppIntent { await VPNSnoozeLiveActivityManager().endSnoozeActivity() - var iterations = 0 - - while iterations <= 10 { - try? await Task.sleep(interval: .seconds(0.5)) - - if manager.connection.status == .disconnected { - DailyPixel.fire(pixel: .networkProtectionWidgetDisconnectSuccess) - return .result() - } - - iterations += 1 - } - VPNReloadStatusWidgets() + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectSuccess) + return .result() + } catch DisableAttemptFailure.cancelled { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectCancelled) return .result() } catch { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectFailure, error: error) return .result() } } } +/// `ForegroundContinuableIntent` isn't available for extensions, which makes it impossible to call +/// from extensions. This is the recommended workaround from: +/// https://mastodon.social/@mgorbach/110812347476671807 +/// +@available(iOS 17.0, *) +struct EnableVPNIntent: AppIntent {} + @available(iOS 17.0, *) -struct EnableVPNIntent: AppIntent { +@available(iOSApplicationExtension, unavailable) +extension EnableVPNIntent: ForegroundContinuableIntent { + + private enum EnableAttemptFailure: CustomNSError { + case firstSetupNeeded + } static let title: LocalizedStringResource = "Enable DuckDuckGo VPN" static let description: LocalizedStringResource = "Enables the DuckDuckGo VPN" @@ -84,11 +92,11 @@ struct EnableVPNIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { do { - DailyPixel.fire(pixel: .networkProtectionWidgetConnectAttempt) + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectAttempt) let managers = try await NETunnelProviderManager.loadAllFromPreferences() guard let manager = managers.first else { - return .result() + throw EnableAttemptFailure.firstSetupNeeded } manager.isOnDemandEnabled = true @@ -97,27 +105,22 @@ struct EnableVPNIntent: AppIntent { await VPNSnoozeLiveActivityManager().endSnoozeActivity() - var iterations = 0 + VPNReloadStatusWidgets() - while iterations <= 10 { - try? await Task.sleep(interval: .seconds(0.5)) + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectSuccess) + return .result() + } catch EnableAttemptFailure.firstSetupNeeded { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) - if manager.connection.status == .connected { - DailyPixel.fire(pixel: .networkProtectionWidgetConnectSuccess) - return .result() - } + throw needsToContinueInForegroundError("You need to first enable the VPN from within the DuckDuckGo app.") { - iterations += 1 + await UIApplication.shared.open(AppDeepLinkSchemes.openVPN.url) } - - VPNReloadStatusWidgets() - - return .result() } catch { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectFailure, error: error) return .result() } } - } // MARK: - Snooze From e8b3a2d75db9a08bcf0f8af88113ab36f92008be Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 16 Dec 2024 10:20:41 -0300 Subject: [PATCH 4/8] Updates a copy --- DuckDuckGo/VPNIntents.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/VPNIntents.swift b/DuckDuckGo/VPNIntents.swift index 94f75e7c15..d293e0604a 100644 --- a/DuckDuckGo/VPNIntents.swift +++ b/DuckDuckGo/VPNIntents.swift @@ -112,7 +112,7 @@ extension EnableVPNIntent: ForegroundContinuableIntent { } catch EnableAttemptFailure.firstSetupNeeded { DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) - throw needsToContinueInForegroundError("You need to first enable the VPN from within the DuckDuckGo app.") { + throw needsToContinueInForegroundError("You need to enable the VPN from the DuckDuckGo App.") { await UIApplication.shared.open(AppDeepLinkSchemes.openVPN.url) } From c5704a90acddb88c782031406f43c2ea2228b45d Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 16 Dec 2024 11:28:56 -0300 Subject: [PATCH 5/8] WIP --- DuckDuckGo.xcodeproj/project.pbxproj | 12 ++++ DuckDuckGo/VPNIntentTunnelController.swift | 63 +++++++++++++++++ DuckDuckGo/VPNIntents.swift | 57 +++++---------- DuckDuckGo/VPNToggleIntent.swift | 80 ++++++++-------------- Widgets/UserTextShared.swift | 24 +++++++ Widgets/en.lproj/Localizable.strings | 3 + 6 files changed, 150 insertions(+), 89 deletions(-) create mode 100644 DuckDuckGo/VPNIntentTunnelController.swift create mode 100644 Widgets/UserTextShared.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0113127b54..7cd30c1591 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -377,6 +377,10 @@ 7B1604E82CB685B400A44EC6 /* Logger+TipKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */; }; 7B1604EC2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */; }; 7B1604EE2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604ED2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift */; }; + 7B1680FF2D106333005EAE24 /* VPNIntentTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1680FD2D106333005EAE24 /* VPNIntentTunnelController.swift */; }; + 7B1681012D106CB9005EAE24 /* UserTextShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1681002D106CB4005EAE24 /* UserTextShared.swift */; }; + 7B1681022D106CCC005EAE24 /* UserTextShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1681002D106CB4005EAE24 /* UserTextShared.swift */; }; + 7B1681032D106E1D005EAE24 /* VPNIntentTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1680FD2D106333005EAE24 /* VPNIntentTunnelController.swift */; }; 7B1C892C2CF714AA0008224E /* VPNTipsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1C892B2CF714AA0008224E /* VPNTipsModel.swift */; }; 7B4DC5BD2CB29D8400EE5CC2 /* VPNToggleIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */; }; 7B4DC5BE2CB29D8400EE5CC2 /* VPNToggleIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */; }; @@ -1736,6 +1740,8 @@ 7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+TipKit.swift"; sourceTree = ""; }; 7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TipKitController+ConvenienceInitializers.swift"; sourceTree = ""; }; 7B1604ED2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitDebugOptionsUIActionHandling.swift; sourceTree = ""; }; + 7B1680FD2D106333005EAE24 /* VPNIntentTunnelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNIntentTunnelController.swift; sourceTree = ""; }; + 7B1681002D106CB4005EAE24 /* UserTextShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTextShared.swift; sourceTree = ""; }; 7B1C892B2CF714AA0008224E /* VPNTipsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNTipsModel.swift; sourceTree = ""; }; 7B1D7A912D0C723B00E48644 /* DesignResourcesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DesignResourcesKit; path = ../DesignResourcesKit; sourceTree = SOURCE_ROOT; }; 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNToggleIntent.swift; sourceTree = ""; }; @@ -3844,6 +3850,7 @@ 4B5C46282AF2A6DB002A4432 /* Intents */ = { isa = PBXGroup; children = ( + 7B1680FD2D106333005EAE24 /* VPNIntentTunnelController.swift */, 4B5C46292AF2A6E6002A4432 /* VPNIntents.swift */, 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */, 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */, @@ -4463,6 +4470,7 @@ 98B001A8251EABB40090EC07 /* Localizable.strings */, 85DB12EA2A1FE2A4000A4A72 /* LockScreenWidgets.swift */, 8544C37A250B823600A0FE73 /* UserText.swift */, + 7B1681002D106CB4005EAE24 /* UserTextShared.swift */, 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */, 8512EA5324ED30D20073EE19 /* Widgets.swift */, 853273AF24FEFE4600E3C778 /* WidgetsExtension.entitlements */, @@ -7614,6 +7622,7 @@ 7B8E0EC62CC81B4900B2B722 /* TipKitController.swift in Sources */, 7B1604EC2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift in Sources */, 851672D12BED1FC900592F24 /* AutocompleteView.swift in Sources */, + 7B1681022D106CCC005EAE24 /* UserTextShared.swift in Sources */, 3161D13227AC161B00285CF6 /* DownloadMetadata.swift in Sources */, D664C7C72B289AA200CBFA76 /* PurchaseInProgressView.swift in Sources */, 7BFD5FD72C9DB9D7000FF959 /* VPNGeoswitchingTip.swift in Sources */, @@ -7964,6 +7973,7 @@ 3158461A281B08F5004ADB8B /* AutofillLoginListViewModel.swift in Sources */, 31C138A827A3E9C900FFD4B2 /* URLDownloadSession.swift in Sources */, 981FED76220464EF008488D7 /* AutoClearSettingsModel.swift in Sources */, + 7B1680FF2D106333005EAE24 /* VPNIntentTunnelController.swift in Sources */, 83004E882193E8C700DA013C /* TabViewControllerLongPressMenuExtension.swift in Sources */, 98F78B8E22419093007CACF4 /* ThemableNavigationController.swift in Sources */, CBD4F140279EBFB300B20FD7 /* SwiftUICollectionViewCell.swift in Sources */, @@ -8411,6 +8421,7 @@ files = ( 853273AE24FEF49600E3C778 /* ColorExtension.swift in Sources */, 4BD96E0F2C4DCFEB003BC32C /* VPNSnoozeActivityAttributes.swift in Sources */, + 7B1681012D106CB9005EAE24 /* UserTextShared.swift in Sources */, 7BC0BB982D08854400445624 /* VPNIntents.swift in Sources */, 373608932ABB432600629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */, 4BD96E102C4DF329003BC32C /* VPNSnoozeLiveActivityManager.swift in Sources */, @@ -8421,6 +8432,7 @@ 7B4DC5C02CB2A4A500EE5CC2 /* VPNStatusValueProvider.swift in Sources */, 853273B424FFB36100E3C778 /* UIColorExtension.swift in Sources */, 853273AB24FEF27500E3C778 /* WidgetViews.swift in Sources */, + 7B1681032D106E1D005EAE24 /* VPNIntentTunnelController.swift in Sources */, 4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */, 8512EA5424ED30D20073EE19 /* Widgets.swift in Sources */, 85DB12EB2A1FE2A4000A4A72 /* LockScreenWidgets.swift in Sources */, diff --git a/DuckDuckGo/VPNIntentTunnelController.swift b/DuckDuckGo/VPNIntentTunnelController.swift new file mode 100644 index 0000000000..03884adfe9 --- /dev/null +++ b/DuckDuckGo/VPNIntentTunnelController.swift @@ -0,0 +1,63 @@ +// +// VPNIntentTunnelController.swift +// DuckDuckGo +// +// 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 Core +import NetworkExtension + +@available(iOS 17.0, *) +struct VPNIntentTunnelController { + + enum StartFailure: CustomNSError { + case vpnNotConfigured + } + + enum StopFailure: CustomNSError { + case vpnNotConfigured + } + + func start() async throws { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + guard let manager = managers.first else { + throw StartFailure.vpnNotConfigured + } + + manager.isOnDemandEnabled = true + try await manager.saveToPreferences() + try manager.connection.startVPNTunnel() + + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + + VPNReloadStatusWidgets() + } + + func stop() async throws { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + guard let manager = managers.first else { + throw StopFailure.vpnNotConfigured + } + + manager.isOnDemandEnabled = false + try await manager.saveToPreferences() + manager.connection.stopVPNTunnel() + + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + + VPNReloadStatusWidgets() + } +} diff --git a/DuckDuckGo/VPNIntents.swift b/DuckDuckGo/VPNIntents.swift index d293e0604a..f3bca6ad79 100644 --- a/DuckDuckGo/VPNIntents.swift +++ b/DuckDuckGo/VPNIntents.swift @@ -43,22 +43,12 @@ struct DisableVPNIntent: AppIntent { do { DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectAttempt) - let managers = try await NETunnelProviderManager.loadAllFromPreferences() - guard let manager = managers.first else { - throw DisableAttemptFailure.cancelled - } - - manager.isOnDemandEnabled = false - try await manager.saveToPreferences() - manager.connection.stopVPNTunnel() - - await VPNSnoozeLiveActivityManager().endSnoozeActivity() - - VPNReloadStatusWidgets() + let controller = VPNIntentTunnelController() + try await controller.stop() DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectSuccess) return .result() - } catch DisableAttemptFailure.cancelled { + } catch VPNIntentTunnelController.StopFailure.vpnNotConfigured { DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectCancelled) return .result() } catch { @@ -78,11 +68,6 @@ struct EnableVPNIntent: AppIntent {} @available(iOS 17.0, *) @available(iOSApplicationExtension, unavailable) extension EnableVPNIntent: ForegroundContinuableIntent { - - private enum EnableAttemptFailure: CustomNSError { - case firstSetupNeeded - } - static let title: LocalizedStringResource = "Enable DuckDuckGo VPN" static let description: LocalizedStringResource = "Enables the DuckDuckGo VPN" static let openAppWhenRun: Bool = false @@ -94,31 +79,25 @@ extension EnableVPNIntent: ForegroundContinuableIntent { do { DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectAttempt) - let managers = try await NETunnelProviderManager.loadAllFromPreferences() - guard let manager = managers.first else { - throw EnableAttemptFailure.firstSetupNeeded - } - - manager.isOnDemandEnabled = true - try await manager.saveToPreferences() - try manager.connection.startVPNTunnel() - - await VPNSnoozeLiveActivityManager().endSnoozeActivity() - - VPNReloadStatusWidgets() + let controller = VPNIntentTunnelController() + try await controller.start() DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectSuccess) return .result() - } catch EnableAttemptFailure.firstSetupNeeded { - DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) - - throw needsToContinueInForegroundError("You need to enable the VPN from the DuckDuckGo App.") { - - await UIApplication.shared.open(AppDeepLinkSchemes.openVPN.url) - } } catch { - DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectFailure, error: error) - return .result() + switch error { + case VPNIntentTunnelController.StartFailure.vpnNotConfigured: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) + + let dialog = IntentDialog(stringLiteral: UserText.vpnNeedsToBeEnabledFromApp) + throw needsToContinueInForegroundError(dialog) { + await UIApplication.shared.open(AppDeepLinkSchemes.openVPN.url) + } + default: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectFailure, error: error) + + throw error + } } } } diff --git a/DuckDuckGo/VPNToggleIntent.swift b/DuckDuckGo/VPNToggleIntent.swift index f49c282ee3..335c98a930 100644 --- a/DuckDuckGo/VPNToggleIntent.swift +++ b/DuckDuckGo/VPNToggleIntent.swift @@ -25,72 +25,52 @@ import Core // MARK: - Toggle + +/// `ForegroundContinuableIntent` isn't available for extensions, which makes it impossible to call +/// from extensions. This is the recommended workaround from: +/// https://mastodon.social/@mgorbach/110812347476671807 +/// @available(iOS 17.0, *) struct VPNToggleIntent: SetValueIntent { + @Parameter(title: "Enabled") + var value: Bool +} + +@available(iOS 17.0, *) +@available(iOSApplicationExtension, unavailable) +extension VPNToggleIntent: SetValueIntent & ForegroundContinuableIntent { static let title: LocalizedStringResource = "Toggle DuckDuckGo VPN" static let description: LocalizedStringResource = "Toggles the DuckDuckGo VPN" static let isDiscoverable: Bool = false - @Parameter(title: "Enabled") - var value: Bool - @MainActor func perform() async throws -> some IntentResult { - if value { - try await enableVPN() - } else { - try await disableVPN() - } - - await VPNSnoozeLiveActivityManager().endSnoozeActivity() - - return .result() - } - - func enableVPN() async throws { - let managers = try await NETunnelProviderManager.loadAllFromPreferences() - guard let manager = managers.first else { - return - } - - manager.isOnDemandEnabled = true - try await manager.saveToPreferences() - try manager.connection.startVPNTunnel() - } - - func disableVPN() async throws { do { - //DailyPixel.fire(pixel: .networkProtectionWidgetDisconnectAttempt) - - let managers = try await NETunnelProviderManager.loadAllFromPreferences() - guard let manager = managers.first else { - return - } + //DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectAttempt) - //manager.connection.status + let controller = VPNIntentTunnelController() - manager.isOnDemandEnabled = false - try await manager.saveToPreferences() - manager.connection.stopVPNTunnel() - - await VPNSnoozeLiveActivityManager().endSnoozeActivity() - - var iterations = 0 + if value { + try await controller.start() + } else { + try await controller.stop() + } - while iterations <= 10 { - try? await Task.sleep(interval: .seconds(0.5)) + return .result() + } catch { + switch error { + case VPNIntentTunnelController.StartFailure.vpnNotConfigured: + //DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) - if manager.connection.status == .disconnected { - //DailyPixel.fire(pixel: .networkProtectionWidgetDisconnectSuccess) - return + let dialog = IntentDialog(stringLiteral: UserText.vpnNeedsToBeEnabledFromApp) + throw needsToContinueInForegroundError() { + await UIApplication.shared.open(AppDeepLinkSchemes.openVPN.url) } + default: + //DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectFailure, error: error) - iterations += 1 + throw error } - - VPNReloadStatusWidgets() - } catch { - // no-op } } } diff --git a/Widgets/UserTextShared.swift b/Widgets/UserTextShared.swift new file mode 100644 index 0000000000..ed0249921b --- /dev/null +++ b/Widgets/UserTextShared.swift @@ -0,0 +1,24 @@ +// +// UserTextShared.swift +// DuckDuckGo +// +// 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 + +extension UserText { + static let vpnNeedsToBeEnabledFromApp = NSLocalizedString("intent.vpn.needs.to.be.enabled.from.app", value: "You need to enable the VPN from the DuckDuckGo App.", comment: "Message that comes up when trying to enable the VPN from intents, asking the user to enable it from the app so it's configured") +} diff --git a/Widgets/en.lproj/Localizable.strings b/Widgets/en.lproj/Localizable.strings index 0b080e452e..9fd16ec9ad 100644 --- a/Widgets/en.lproj/Localizable.strings +++ b/Widgets/en.lproj/Localizable.strings @@ -1,3 +1,6 @@ +/* Message that comes up when trying to enable the VPN from intents, asking the user to enable it from the app so it's configured */ +"intent.vpn.needs.to.be.enabled.from.app" = "You need to enable the VPN from the DuckDuckGo App."; + /* Description shown to the user when adding the Email Protection lock screen widget */ "lock.screen.widget.email.description" = "Instantly generate a new private Duck Address."; From b57e2a586b4df0d5f4dfc0074975ddfbeb0fcb9f Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 16 Dec 2024 12:29:30 -0300 Subject: [PATCH 6/8] Adds pixels to track CC usage for the VPN --- Core/PixelEvent.swift | 20 ++++++++++++++ DuckDuckGo/VPNToggleIntent.swift | 47 ++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 5d9d3139b1..e2781679c6 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -470,6 +470,16 @@ extension Pixel { case networkProtectionWidgetDisconnectCancelled case networkProtectionWidgetDisconnectFailure + case vpnControlCenterConnectAttempt + case vpnControlCenterConnectSuccess + case vpnControlCenterConnectCancelled + case vpnControlCenterConnectFailure + + case vpnControlCenterDisconnectAttempt + case vpnControlCenterDisconnectSuccess + case vpnControlCenterDisconnectCancelled + case vpnControlCenterDisconnectFailure + case networkProtectionDNSUpdateCustom case networkProtectionDNSUpdateDefault @@ -1671,6 +1681,16 @@ extension Pixel.Event { case .networkProtectionWidgetDisconnectCancelled: return "m_netp_widget_disconnect_cancelled" case .networkProtectionWidgetDisconnectFailure: return "m_netp_widget_disconnect_failure" + case .vpnControlCenterConnectAttempt: return "m_vpn_control-center_connect_attempt" + case .vpnControlCenterConnectSuccess: return "m_vpn_control-center_connect_success" + case .vpnControlCenterConnectCancelled: return "m_vpn_control-center_connect_cancelled" + case .vpnControlCenterConnectFailure: return "m_vpn_control-center_connect_failure" + + case .vpnControlCenterDisconnectAttempt: return "m_vpn_control-center_disconnect_attempt" + case .vpnControlCenterDisconnectSuccess: return "m_vpn_control-center_disconnect_success" + case .vpnControlCenterDisconnectCancelled: return "m_vpn_control-center_disconnect_cancelled" + case .vpnControlCenterDisconnectFailure: return "m_vpn_control-center_disconnect_failure" + // MARK: Secure Vault case .secureVaultL1KeyMigration: return "m_secure-vault_keystore_event_l1-key-migration" case .secureVaultL2KeyMigration: return "m_secure-vault_keystore_event_l2-key-migration" diff --git a/DuckDuckGo/VPNToggleIntent.swift b/DuckDuckGo/VPNToggleIntent.swift index 335c98a930..edaff7c968 100644 --- a/DuckDuckGo/VPNToggleIntent.swift +++ b/DuckDuckGo/VPNToggleIntent.swift @@ -22,6 +22,7 @@ import NetworkExtension import NetworkProtection import WidgetKit import Core +import OSLog // MARK: - Toggle @@ -45,30 +46,52 @@ extension VPNToggleIntent: SetValueIntent & ForegroundContinuableIntent { @MainActor func perform() async throws -> some IntentResult { - do { - //DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectAttempt) + if value { + try await startVPN() + } else { + try await stopVPN() + } - let controller = VPNIntentTunnelController() + return .result() + } - if value { - try await controller.start() - } else { - try await controller.stop() - } + private func startVPN() async throws { + do { + DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectAttempt) - return .result() + let controller = VPNIntentTunnelController() + try await controller.start() + DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectSuccess) } catch { switch error { case VPNIntentTunnelController.StartFailure.vpnNotConfigured: - //DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) + DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectCancelled) let dialog = IntentDialog(stringLiteral: UserText.vpnNeedsToBeEnabledFromApp) - throw needsToContinueInForegroundError() { + throw needsToContinueInForegroundError(dialog) { await UIApplication.shared.open(AppDeepLinkSchemes.openVPN.url) } default: - //DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectFailure, error: error) + DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectFailure, error: error) + throw error + } + } + } + private func stopVPN() async throws { + do { + DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterDisconnectAttempt) + + let controller = VPNIntentTunnelController() + try await controller.stop() + DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterDisconnectSuccess) + } catch { + switch error { + case VPNIntentTunnelController.StopFailure.vpnNotConfigured: + DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterDisconnectCancelled) + throw error + default: + DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterDisconnectFailure, error: error) throw error } } From b798fb0139879cf8bc0eb801518a6a6f67a5d186 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 16 Dec 2024 18:24:39 -0300 Subject: [PATCH 7/8] WIP --- DuckDuckGo.xcodeproj/project.pbxproj | 24 ++-- DuckDuckGo/Info.plist | 1 - DuckDuckGo/VPNAppIntents.swift | 108 ++++++++++++++++++ DuckDuckGo/VPNAutoShortcuts.swift | 4 +- DuckDuckGo/VPNToggleIntent.swift | 20 +--- Widgets/VPNWidget.swift | 6 +- .../VPNWidgetIntents.swift | 31 +++-- 7 files changed, 148 insertions(+), 46 deletions(-) create mode 100644 DuckDuckGo/VPNAppIntents.swift rename DuckDuckGo/VPNIntents.swift => Widgets/VPNWidgetIntents.swift (86%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7cd30c1591..7ca4eef0f6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -234,7 +234,6 @@ 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 */; }; 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 */; }; @@ -381,10 +380,14 @@ 7B1681012D106CB9005EAE24 /* UserTextShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1681002D106CB4005EAE24 /* UserTextShared.swift */; }; 7B1681022D106CCC005EAE24 /* UserTextShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1681002D106CB4005EAE24 /* UserTextShared.swift */; }; 7B1681032D106E1D005EAE24 /* VPNIntentTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1680FD2D106333005EAE24 /* VPNIntentTunnelController.swift */; }; + 7B1681062D10BC96005EAE24 /* VPNAppIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1681042D10BC7B005EAE24 /* VPNAppIntents.swift */; }; + 7B1681092D10C678005EAE24 /* VPNStatusValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5BF2CB2A4A500EE5CC2 /* VPNStatusValueProvider.swift */; }; + 7B16810A2D10C680005EAE24 /* VPNControlWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFC32AF2CB291BB007A8E17 /* VPNControlWidget.swift */; }; + 7B16810C2D10CF44005EAE24 /* VPNWidgetIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B16810B2D10CF44005EAE24 /* VPNWidgetIntents.swift */; }; + 7B16810D2D10CF44005EAE24 /* VPNWidgetIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B16810B2D10CF44005EAE24 /* VPNWidgetIntents.swift */; }; 7B1C892C2CF714AA0008224E /* VPNTipsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1C892B2CF714AA0008224E /* VPNTipsModel.swift */; }; 7B4DC5BD2CB29D8400EE5CC2 /* VPNToggleIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */; }; 7B4DC5BE2CB29D8400EE5CC2 /* VPNToggleIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */; }; - 7B4DC5C02CB2A4A500EE5CC2 /* VPNStatusValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5BF2CB2A4A500EE5CC2 /* VPNStatusValueProvider.swift */; }; 7B4DC5C22CB2AE4600EE5CC2 /* WidgetKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */; }; 7B4DC5C32CB2AF0700EE5CC2 /* WidgetKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */; }; 7B4DC5C42CB2B1D000EE5CC2 /* WidgetKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */; }; @@ -394,12 +397,10 @@ 7B4F87EC2D07396A0010B18F /* SiriEducation.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B4F87EB2D07396A0010B18F /* SiriEducation.xcassets */; }; 7B4F87EE2D0739EB0010B18F /* SiriBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4F87ED2D0739E80010B18F /* SiriBubbleView.swift */; }; 7B8E0EC62CC81B4900B2B722 /* TipKitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8E0EC52CC81B4800B2B722 /* TipKitController.swift */; }; - 7BC0BB982D08854400445624 /* VPNIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5C46292AF2A6E6002A4432 /* VPNIntents.swift */; }; 7BC571202BDBB877003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; 7BDBAD0E2CBFB3F1000379B7 /* VPN.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7BDBAD0D2CBFB3F1000379B7 /* VPN.xcassets */; }; 7BF78E022CA2CC3E0026A1FC /* TipKitAppEventHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF78E012CA2CC3E0026A1FC /* TipKitAppEventHandling.swift */; }; - 7BFC32B02CB291BB007A8E17 /* VPNControlWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFC32AF2CB291BB007A8E17 /* VPNControlWidget.swift */; }; 7BFD5FD52C9DA310000FF959 /* VPNAddWidgetTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD5FD42C9DA310000FF959 /* VPNAddWidgetTip.swift */; }; 7BFD5FD72C9DB9D7000FF959 /* VPNGeoswitchingTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD5FD62C9DB9D7000FF959 /* VPNGeoswitchingTip.swift */; }; 7BFD5FD92C9DBC24000FF959 /* VPNSnoozeTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD5FD82C9DBC24000FF959 /* VPNSnoozeTip.swift */; }; @@ -1606,7 +1607,6 @@ 4B412ACB2BBB3D0900A39F5E /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.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 = ""; }; @@ -1742,6 +1742,8 @@ 7B1604ED2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitDebugOptionsUIActionHandling.swift; sourceTree = ""; }; 7B1680FD2D106333005EAE24 /* VPNIntentTunnelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNIntentTunnelController.swift; sourceTree = ""; }; 7B1681002D106CB4005EAE24 /* UserTextShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTextShared.swift; sourceTree = ""; }; + 7B1681042D10BC7B005EAE24 /* VPNAppIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNAppIntents.swift; sourceTree = ""; }; + 7B16810B2D10CF44005EAE24 /* VPNWidgetIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWidgetIntents.swift; sourceTree = ""; }; 7B1C892B2CF714AA0008224E /* VPNTipsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNTipsModel.swift; sourceTree = ""; }; 7B1D7A912D0C723B00E48644 /* DesignResourcesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DesignResourcesKit; path = ../DesignResourcesKit; sourceTree = SOURCE_ROOT; }; 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNToggleIntent.swift; sourceTree = ""; }; @@ -3851,7 +3853,7 @@ isa = PBXGroup; children = ( 7B1680FD2D106333005EAE24 /* VPNIntentTunnelController.swift */, - 4B5C46292AF2A6E6002A4432 /* VPNIntents.swift */, + 7B1681042D10BC7B005EAE24 /* VPNAppIntents.swift */, 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */, 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */, ); @@ -4476,6 +4478,7 @@ 853273AF24FEFE4600E3C778 /* WidgetsExtension.entitlements */, 853273A924FEF24300E3C778 /* WidgetViews.swift */, 7B4DC5BF2CB2A4A500EE5CC2 /* VPNStatusValueProvider.swift */, + 7B16810B2D10CF44005EAE24 /* VPNWidgetIntents.swift */, 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */, 7BFC32AF2CB291BB007A8E17 /* VPNControlWidget.swift */, ); @@ -7852,6 +7855,7 @@ 1EEF12502851016B003DDE57 /* PrivacyIconAndTrackersAnimator.swift in Sources */, 31CB4251273AF50700FA0F3F /* SpeechRecognizerProtocol.swift in Sources */, 319A37172829C8AD0079FBCE /* UITableViewExtension.swift in Sources */, + 7B1681062D10BC96005EAE24 /* VPNAppIntents.swift in Sources */, 85EE7F59224673C5000FE757 /* WebContainerNavigationController.swift in Sources */, 6FD1BAE52B87A107000C475C /* AdAttributionReporterStorage.swift in Sources */, 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift in Sources */, @@ -8080,7 +8084,6 @@ 8562CE152B9B645C00E1D399 /* CachedBookmarkSuggestions.swift in Sources */, C13F3F682B7F88100083BE40 /* AuthConfirmationPromptView.swift in Sources */, F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */, - 4B5C462A2AF2A6E6002A4432 /* VPNIntents.swift in Sources */, 6FF9AD3F2CE63DD800C5A406 /* TabSwitcherOpenDailyPixel.swift in Sources */, 310742A62848CD780012660B /* BackForwardMenuHistoryItem.swift in Sources */, 9FDEC7B82C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift in Sources */, @@ -8151,6 +8154,7 @@ 8590CB67268A2E520089F6BF /* RootDebugViewController.swift in Sources */, 1DEAADEA2BA4539800E25A97 /* SettingsAppearanceView.swift in Sources */, B623C1C22862CA9E0043013E /* DownloadSession.swift in Sources */, + 7B16810C2D10CF44005EAE24 /* VPNWidgetIntents.swift in Sources */, 9F7CFF7F2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift in Sources */, 985892522260B1B200EEB31B /* ProgressView.swift in Sources */, 85BA585A1F3506AE00C6E8CA /* AppSettings.swift in Sources */, @@ -8422,17 +8426,17 @@ 853273AE24FEF49600E3C778 /* ColorExtension.swift in Sources */, 4BD96E0F2C4DCFEB003BC32C /* VPNSnoozeActivityAttributes.swift in Sources */, 7B1681012D106CB9005EAE24 /* UserTextShared.swift in Sources */, - 7BC0BB982D08854400445624 /* VPNIntents.swift in Sources */, 373608932ABB432600629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */, 4BD96E102C4DF329003BC32C /* VPNSnoozeLiveActivityManager.swift in Sources */, - 7BFC32B02CB291BB007A8E17 /* VPNControlWidget.swift in Sources */, + 7B16810A2D10C680005EAE24 /* VPNControlWidget.swift in Sources */, 853273B324FF114700E3C778 /* DeepLinks.swift in Sources */, 7B4DC5BD2CB29D8400EE5CC2 /* VPNToggleIntent.swift in Sources */, 7B4DC5C22CB2AE4600EE5CC2 /* WidgetKind.swift in Sources */, - 7B4DC5C02CB2A4A500EE5CC2 /* VPNStatusValueProvider.swift in Sources */, 853273B424FFB36100E3C778 /* UIColorExtension.swift in Sources */, 853273AB24FEF27500E3C778 /* WidgetViews.swift in Sources */, + 7B1681092D10C678005EAE24 /* VPNStatusValueProvider.swift in Sources */, 7B1681032D106E1D005EAE24 /* VPNIntentTunnelController.swift in Sources */, + 7B16810D2D10CF44005EAE24 /* VPNWidgetIntents.swift in Sources */, 4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */, 8512EA5424ED30D20073EE19 /* Widgets.swift in Sources */, 85DB12EB2A1FE2A4000A4A72 /* LockScreenWidgets.swift in Sources */, diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist index 533d276e76..9fbb5418be 100644 --- a/DuckDuckGo/Info.plist +++ b/DuckDuckGo/Info.plist @@ -197,7 +197,6 @@ CancelSnoozeLiveActivityAppIntentIntent ConfigurationIntent - EnableVPNIntentIntent SUBSCRIPTION_APP_GROUP $(AppIdentifierPrefix)$(SUBSCRIPTION_APP_GROUP) diff --git a/DuckDuckGo/VPNAppIntents.swift b/DuckDuckGo/VPNAppIntents.swift new file mode 100644 index 0000000000..9d31388256 --- /dev/null +++ b/DuckDuckGo/VPNAppIntents.swift @@ -0,0 +1,108 @@ +// +// VPNSiriIntents.swift +// DuckDuckGo +// +// 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 AppIntents +import NetworkExtension +import NetworkProtection +import WidgetKit +import Core + +// MARK: - Enable & Disable + +/// App intent to disable the VPN +/// +/// This is used in App Shortcuts, for things like Shortcuts.app, Spotlight and Siri. +/// This is very similar to ``WidgetVPNDisableIntent``, but this runs in-app, allows continuation in the app if needed, +/// and provides a result dialog. +/// +@available(iOS 17.0, *) +struct DisableVPNAppIntent: AppIntent { + + private enum DisableAttemptFailure: CustomNSError { + case cancelled + } + + static let title: LocalizedStringResource = "Disable DuckDuckGo VPN" + static let description: LocalizedStringResource = "Disables the DuckDuckGo VPN" + static let openAppWhenRun: Bool = false + static let isDiscoverable: Bool = true + static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication + + @MainActor + func perform() async throws -> some IntentResult & ProvidesDialog { + do { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectAttempt) + + let controller = VPNIntentTunnelController() + try await controller.stop() + + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectSuccess) + return .result(dialog: "DuckDuckGo VPN is disconnecting...") + } catch VPNIntentTunnelController.StopFailure.vpnNotConfigured { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectCancelled) + return .result(dialog: "The DuckDuckGo VPN is not connected") + } catch { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectFailure, error: error) + throw error + } + } +} + +/// App intent to enable the VPN +/// +/// This is used in App Shortcuts, for things like Shortcuts.app, Spotlight and Siri. +/// This is very similar to ``VPNWidgetEnableIntent``, but this runs in-app, allows continuation in the app if needed, +/// and provides a result dialog. +/// +@available(iOS 17.0, *) +@available(iOSApplicationExtension, unavailable) +struct EnableVPNAppIntent: ForegroundContinuableIntent { + static let title: LocalizedStringResource = "Enable DuckDuckGo VPN" + static let description: LocalizedStringResource = "Enables the DuckDuckGo VPN" + static let openAppWhenRun: Bool = false + static let isDiscoverable: Bool = true + static var authenticationPolicy: IntentAuthenticationPolicy = .alwaysAllowed + + @MainActor + func perform() async throws -> some IntentResult & ProvidesDialog { + do { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectAttempt) + + let controller = VPNIntentTunnelController() + try await controller.start() + + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectSuccess) + return .result(dialog: "DuckDuckGo VPN is connecting...") + } catch { + switch error { + case VPNIntentTunnelController.StartFailure.vpnNotConfigured: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) + + let dialog = IntentDialog(stringLiteral: UserText.vpnNeedsToBeEnabledFromApp) + throw needsToContinueInForegroundError(dialog) { + await UIApplication.shared.open(AppDeepLinkSchemes.openVPN.url) + } + default: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectFailure, error: error) + + throw error + } + } + } +} diff --git a/DuckDuckGo/VPNAutoShortcuts.swift b/DuckDuckGo/VPNAutoShortcuts.swift index b130f63dda..05fd7479e2 100644 --- a/DuckDuckGo/VPNAutoShortcuts.swift +++ b/DuckDuckGo/VPNAutoShortcuts.swift @@ -25,7 +25,7 @@ struct VPNAutoShortcutsiOS17: AppShortcutsProvider { @AppShortcutsBuilder static var appShortcuts: [AppShortcut] { - AppShortcut(intent: EnableVPNIntent(), + AppShortcut(intent: EnableVPNAppIntent(), phrases: [ "Connect \(.applicationName) VPN", "Connect the \(.applicationName) VPN", @@ -42,7 +42,7 @@ struct VPNAutoShortcutsiOS17: AppShortcutsProvider { "Protect my connection with \(.applicationName)" ], systemImageName: "globe") - AppShortcut(intent: DisableVPNIntent(), + AppShortcut(intent: DisableVPNAppIntent(), phrases: [ "Disconnect \(.applicationName) VPN", "Disconnect the \(.applicationName) VPN", diff --git a/DuckDuckGo/VPNToggleIntent.swift b/DuckDuckGo/VPNToggleIntent.swift index edaff7c968..c44a76dd66 100644 --- a/DuckDuckGo/VPNToggleIntent.swift +++ b/DuckDuckGo/VPNToggleIntent.swift @@ -26,33 +26,28 @@ import OSLog // MARK: - Toggle - /// `ForegroundContinuableIntent` isn't available for extensions, which makes it impossible to call /// from extensions. This is the recommended workaround from: /// https://mastodon.social/@mgorbach/110812347476671807 /// @available(iOS 17.0, *) struct VPNToggleIntent: SetValueIntent { - @Parameter(title: "Enabled") - var value: Bool -} - -@available(iOS 17.0, *) -@available(iOSApplicationExtension, unavailable) -extension VPNToggleIntent: SetValueIntent & ForegroundContinuableIntent { static let title: LocalizedStringResource = "Toggle DuckDuckGo VPN" static let description: LocalizedStringResource = "Toggles the DuckDuckGo VPN" static let isDiscoverable: Bool = false + @Parameter(title: "Enabled") + var value: Bool + @MainActor func perform() async throws -> some IntentResult { if value { try await startVPN() + return .result() } else { try await stopVPN() + return .result() } - - return .result() } private func startVPN() async throws { @@ -67,10 +62,7 @@ extension VPNToggleIntent: SetValueIntent & ForegroundContinuableIntent { case VPNIntentTunnelController.StartFailure.vpnNotConfigured: DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectCancelled) - let dialog = IntentDialog(stringLiteral: UserText.vpnNeedsToBeEnabledFromApp) - throw needsToContinueInForegroundError(dialog) { - await UIApplication.shared.open(AppDeepLinkSchemes.openVPN.url) - } + throw error default: DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectFailure, error: error) throw error diff --git a/Widgets/VPNWidget.swift b/Widgets/VPNWidget.swift index 00300d3d1a..a2f5af1ab4 100644 --- a/Widgets/VPNWidget.swift +++ b/Widgets/VPNWidget.swift @@ -174,7 +174,7 @@ struct VPNStatusView: View { switch status { case .connected: let buttonTitle = snoozeTimingStore.isSnoozing ? UserText.vpnWidgetLiveActivityWakeUpButton : UserText.vpnWidgetDisconnectButton - let intent: any AppIntent = snoozeTimingStore.isSnoozing ? CancelSnoozeVPNIntent() : DisableVPNIntent() + let intent: any AppIntent = snoozeTimingStore.isSnoozing ? CancelSnoozeVPNIntent() : VPNWidgetDisableIntent() Button(buttonTitle, intent: intent) .font(.system(size: 14, weight: .semibold)) @@ -192,7 +192,7 @@ struct VPNStatusView: View { .padding(.top, 6) .padding(.bottom, 16) case .connecting, .reasserting: - Button(UserText.vpnWidgetDisconnectButton, intent: DisableVPNIntent()) + Button(UserText.vpnWidgetDisconnectButton, intent: VPNWidgetDisableIntent()) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(disconnectButtonForegroundColor(isDisabled: status != .connected)) .buttonStyle(.borderedProminent) @@ -234,7 +234,7 @@ struct VPNStatusView: View { private var connectButton: Button { switch entry.status { case .status: - Button(UserText.vpnWidgetConnectButton, intent: EnableVPNIntent()) + Button(UserText.vpnWidgetConnectButton, intent: VPNWidgetEnableIntent()) case .error, .notConfigured: Button(UserText.vpnWidgetConnectButton) { openURL(DeepLinks.openVPN) diff --git a/DuckDuckGo/VPNIntents.swift b/Widgets/VPNWidgetIntents.swift similarity index 86% rename from DuckDuckGo/VPNIntents.swift rename to Widgets/VPNWidgetIntents.swift index f3bca6ad79..70d77eb270 100644 --- a/DuckDuckGo/VPNIntents.swift +++ b/Widgets/VPNWidgetIntents.swift @@ -25,8 +25,14 @@ import Core // MARK: - Enable & Disable +/// App intent to disable the VPN +/// +/// This is used in our Widget only. +/// This is very similar to ``DisableVPNAppIntent``, but this can run in both widget and app, +/// does not support continuation in the app and does not provide any result dialog. +/// @available(iOS 17.0, *) -struct DisableVPNIntent: AppIntent { +struct VPNWidgetDisableIntent: AppIntent { private enum DisableAttemptFailure: CustomNSError { case cancelled @@ -53,21 +59,19 @@ struct DisableVPNIntent: AppIntent { return .result() } catch { DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectFailure, error: error) - return .result() + throw error } } } -/// `ForegroundContinuableIntent` isn't available for extensions, which makes it impossible to call -/// from extensions. This is the recommended workaround from: -/// https://mastodon.social/@mgorbach/110812347476671807 +/// App intent to disable the VPN +/// +/// This is used in our Widget only. +/// This is very similar to ``DisableVPNAppIntent``, but this can run in both widget and app, +/// does not support continuation in the app and does not provide any result dialog. /// @available(iOS 17.0, *) -struct EnableVPNIntent: AppIntent {} - -@available(iOS 17.0, *) -@available(iOSApplicationExtension, unavailable) -extension EnableVPNIntent: ForegroundContinuableIntent { +struct VPNWidgetEnableIntent: AppIntent { static let title: LocalizedStringResource = "Enable DuckDuckGo VPN" static let description: LocalizedStringResource = "Enables the DuckDuckGo VPN" static let openAppWhenRun: Bool = false @@ -88,14 +92,9 @@ extension EnableVPNIntent: ForegroundContinuableIntent { switch error { case VPNIntentTunnelController.StartFailure.vpnNotConfigured: DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) - - let dialog = IntentDialog(stringLiteral: UserText.vpnNeedsToBeEnabledFromApp) - throw needsToContinueInForegroundError(dialog) { - await UIApplication.shared.open(AppDeepLinkSchemes.openVPN.url) - } + throw error default: DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectFailure, error: error) - throw error } } From 348021b6ad957cce67be1cf25d97c06bdbf04ffa Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 17 Dec 2024 15:52:54 -0300 Subject: [PATCH 8/8] WIP --- DuckDuckGo.xcodeproj/project.pbxproj | 74 +++++--- DuckDuckGo/VPNAutoShortcuts.swift | 12 +- DuckDuckGo/VPNIntentTunnelController.swift | 63 ------- .../{VPNAppIntents.swift => VPNIntents.swift} | 23 ++- DuckDuckGo/VPNSnoozeActivityAttributes.swift | 1 - LocalPackages/VPNiOS/.gitignore | 8 + LocalPackages/VPNiOS/Package.swift | 32 ++++ .../Sources/VPNAppIntents/VPNAppIntents.swift | 92 +++++++++ .../VPNWidgetTunnelController.swift | 96 ++++++++++ .../ControlWidgetVPNIntents.swift | 31 +-- Widgets/VPNControlWidget.swift | 5 +- Widgets/VPNStatusValueProvider.swift | 1 + Widgets/VPNWidget.swift | 6 +- Widgets/WidgetKind.swift | 4 +- ...etIntents.swift => WidgetVPNIntents.swift} | 24 ++- Widgets/Widgets.swift | 9 + .../Contents.json | 5 +- .../VPN-Off.svg | 139 -------------- .../VPNOFF4.svg | 119 ++++++++++++ .../Contents.json | 5 +- .../ControlCenter-VPN-on.symbolset/VPN-On.svg | 177 ------------------ .../ControlCenter-VPN-on.symbolset/VPNON4.svg | 119 ++++++++++++ 22 files changed, 595 insertions(+), 450 deletions(-) delete mode 100644 DuckDuckGo/VPNIntentTunnelController.swift rename DuckDuckGo/{VPNAppIntents.swift => VPNIntents.swift} (84%) create mode 100644 LocalPackages/VPNiOS/.gitignore create mode 100644 LocalPackages/VPNiOS/Package.swift create mode 100644 LocalPackages/VPNiOS/Sources/VPNAppIntents/VPNAppIntents.swift create mode 100644 LocalPackages/VPNiOS/Sources/VPNWidgetSupport/VPNWidgetTunnelController.swift rename DuckDuckGo/VPNToggleIntent.swift => Widgets/ControlWidgetVPNIntents.swift (78%) rename Widgets/{VPNWidgetIntents.swift => WidgetVPNIntents.swift} (88%) delete mode 100644 Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/VPN-Off.svg create mode 100644 Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/VPNOFF4.svg delete mode 100644 Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/VPN-On.svg create mode 100644 Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/VPNON4.svg diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7ca4eef0f6..493731e07b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -197,7 +197,6 @@ 3712091E2C21E390003ADF3D /* RemoteMessagingStoreErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3712091D2C21E390003ADF3D /* RemoteMessagingStoreErrorHandling.swift */; }; 372A0FF02B2389590033BF7F /* SyncMetricsEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372A0FEF2B2389590033BF7F /* SyncMetricsEventsHandler.swift */; }; 373608902ABB1E6C00629E7F /* FavoritesDisplayModeStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3736088F2ABB1E6C00629E7F /* FavoritesDisplayModeStorage.swift */; }; - 373608922ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373608912ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift */; }; 373608932ABB432600629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373608912ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift */; }; 37445F972A155F7C0029F789 /* SyncDataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F962A155F7C0029F789 /* SyncDataProviders.swift */; }; 3760DFED299315EF0045A446 /* Waitlist in Frameworks */ = {isa = PBXBuildFile; productRef = 3760DFEC299315EF0045A446 /* Waitlist */; }; @@ -366,6 +365,7 @@ 6FF9AD3F2CE63DD800C5A406 /* TabSwitcherOpenDailyPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD3E2CE63DC200C5A406 /* TabSwitcherOpenDailyPixel.swift */; }; 6FF9AD412CE6610F00C5A406 /* TabSwitcherOpenDailyPixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD402CE6610600C5A406 /* TabSwitcherOpenDailyPixelTests.swift */; }; 6FF9AD452CE766F700C5A406 /* NewTabPageControllerPixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD442CE766F700C5A406 /* NewTabPageControllerPixelTests.swift */; }; + 7B020B9A2D11F99D00876178 /* FavoritesDisplayMode+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373608912ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift */; }; 7B059F0F2D0387E900371ED0 /* NumberedParagraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B059F0A2D0387E900371ED0 /* NumberedParagraphView.swift */; }; 7B059F112D0387E900371ED0 /* WidgetEducationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B059F0D2D0387E900371ED0 /* WidgetEducationViewController.swift */; }; 7B059F122D0387E900371ED0 /* WidgetEducationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B059F0C2D0387E900371ED0 /* WidgetEducationView.swift */; }; @@ -373,21 +373,23 @@ 7B059F1D2D03A7E400371ED0 /* WidgetsShared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B059F1C2D03A7E400371ED0 /* WidgetsShared.xcassets */; }; 7B059F1E2D03A7E400371ED0 /* WidgetsShared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B059F1C2D03A7E400371ED0 /* WidgetsShared.xcassets */; }; 7B059F222D03BC4400371ED0 /* WidgetEducation.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B059F212D03BC4400371ED0 /* WidgetEducation.xcassets */; }; + 7B10FF242D11A56300F36BF2 /* ControlWidgetVPNIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B10FF232D11A56300F36BF2 /* ControlWidgetVPNIntents.swift */; }; + 7B10FF252D11A56300F36BF2 /* ControlWidgetVPNIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B10FF232D11A56300F36BF2 /* ControlWidgetVPNIntents.swift */; }; 7B1604E82CB685B400A44EC6 /* Logger+TipKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */; }; 7B1604EC2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */; }; 7B1604EE2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604ED2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift */; }; - 7B1680FF2D106333005EAE24 /* VPNIntentTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1680FD2D106333005EAE24 /* VPNIntentTunnelController.swift */; }; 7B1681012D106CB9005EAE24 /* UserTextShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1681002D106CB4005EAE24 /* UserTextShared.swift */; }; 7B1681022D106CCC005EAE24 /* UserTextShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1681002D106CB4005EAE24 /* UserTextShared.swift */; }; - 7B1681032D106E1D005EAE24 /* VPNIntentTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1680FD2D106333005EAE24 /* VPNIntentTunnelController.swift */; }; - 7B1681062D10BC96005EAE24 /* VPNAppIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1681042D10BC7B005EAE24 /* VPNAppIntents.swift */; }; + 7B1681062D10BC96005EAE24 /* VPNIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1681042D10BC7B005EAE24 /* VPNIntents.swift */; }; 7B1681092D10C678005EAE24 /* VPNStatusValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5BF2CB2A4A500EE5CC2 /* VPNStatusValueProvider.swift */; }; 7B16810A2D10C680005EAE24 /* VPNControlWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFC32AF2CB291BB007A8E17 /* VPNControlWidget.swift */; }; - 7B16810C2D10CF44005EAE24 /* VPNWidgetIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B16810B2D10CF44005EAE24 /* VPNWidgetIntents.swift */; }; - 7B16810D2D10CF44005EAE24 /* VPNWidgetIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B16810B2D10CF44005EAE24 /* VPNWidgetIntents.swift */; }; + 7B16810C2D10CF44005EAE24 /* WidgetVPNIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B16810B2D10CF44005EAE24 /* WidgetVPNIntents.swift */; }; + 7B16810D2D10CF44005EAE24 /* WidgetVPNIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B16810B2D10CF44005EAE24 /* WidgetVPNIntents.swift */; }; 7B1C892C2CF714AA0008224E /* VPNTipsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1C892B2CF714AA0008224E /* VPNTipsModel.swift */; }; - 7B4DC5BD2CB29D8400EE5CC2 /* VPNToggleIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */; }; - 7B4DC5BE2CB29D8400EE5CC2 /* VPNToggleIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */; }; + 7B2CCBA32D11ABB100FE5852 /* VPNWidgetSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2CCBA22D11ABB100FE5852 /* VPNWidgetSupport */; }; + 7B2CCBA52D11ABBA00FE5852 /* VPNWidgetSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2CCBA42D11ABBA00FE5852 /* VPNWidgetSupport */; }; + 7B2CCBA72D11F01F00FE5852 /* VPNAppIntents in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2CCBA62D11F01F00FE5852 /* VPNAppIntents */; }; + 7B2CCBA92D11F02800FE5852 /* VPNAppIntents in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2CCBA82D11F02800FE5852 /* VPNAppIntents */; }; 7B4DC5C22CB2AE4600EE5CC2 /* WidgetKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */; }; 7B4DC5C32CB2AF0700EE5CC2 /* WidgetKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */; }; 7B4DC5C42CB2B1D000EE5CC2 /* WidgetKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */; }; @@ -1737,16 +1739,16 @@ 7B059F132D03881000371ED0 /* ControlCenterWidgetEducationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlCenterWidgetEducationView.swift; sourceTree = ""; }; 7B059F1C2D03A7E400371ED0 /* WidgetsShared.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = WidgetsShared.xcassets; sourceTree = ""; }; 7B059F212D03BC4400371ED0 /* WidgetEducation.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = WidgetEducation.xcassets; sourceTree = ""; }; + 7B10FF232D11A56300F36BF2 /* ControlWidgetVPNIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlWidgetVPNIntents.swift; sourceTree = ""; }; + 7B10FF282D11AA0D00F36BF2 /* VPNiOS */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = VPNiOS; sourceTree = ""; }; 7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+TipKit.swift"; sourceTree = ""; }; 7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TipKitController+ConvenienceInitializers.swift"; sourceTree = ""; }; 7B1604ED2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitDebugOptionsUIActionHandling.swift; sourceTree = ""; }; - 7B1680FD2D106333005EAE24 /* VPNIntentTunnelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNIntentTunnelController.swift; sourceTree = ""; }; 7B1681002D106CB4005EAE24 /* UserTextShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTextShared.swift; sourceTree = ""; }; - 7B1681042D10BC7B005EAE24 /* VPNAppIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNAppIntents.swift; sourceTree = ""; }; - 7B16810B2D10CF44005EAE24 /* VPNWidgetIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWidgetIntents.swift; sourceTree = ""; }; + 7B1681042D10BC7B005EAE24 /* VPNIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNIntents.swift; sourceTree = ""; }; + 7B16810B2D10CF44005EAE24 /* WidgetVPNIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetVPNIntents.swift; sourceTree = ""; }; 7B1C892B2CF714AA0008224E /* VPNTipsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNTipsModel.swift; sourceTree = ""; }; 7B1D7A912D0C723B00E48644 /* DesignResourcesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DesignResourcesKit; path = ../DesignResourcesKit; sourceTree = SOURCE_ROOT; }; - 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNToggleIntent.swift; sourceTree = ""; }; 7B4DC5BF2CB2A4A500EE5CC2 /* VPNStatusValueProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatusValueProvider.swift; sourceTree = ""; }; 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetKind.swift; sourceTree = ""; }; 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNAutoShortcuts.swift; sourceTree = ""; }; @@ -3154,6 +3156,8 @@ 0238E44F29C0FAA100615E30 /* FindInPageIOSJSSupport in Frameworks */, 3760DFED299315EF0045A446 /* Waitlist in Frameworks */, F1D43AFA2B99C1D300BAB743 /* BareBonesBrowserKit in Frameworks */, + 7B2CCBA52D11ABBA00FE5852 /* VPNWidgetSupport in Frameworks */, + 7B2CCBA92D11F02800FE5852 /* VPNAppIntents in Frameworks */, F143C2EB1E4A4CD400CFDE3A /* Core.framework in Frameworks */, 31E69A63280F4CB600478327 /* DuckUI in Frameworks */, CB941A6E2B96AB08000F9E7A /* PrivacyDashboard in Frameworks */, @@ -3188,7 +3192,9 @@ files = ( 8512EA5124ED30D20073EE19 /* SwiftUI.framework in Frameworks */, 4BD96E062C4DBC93003BC32C /* NetworkExtension.framework in Frameworks */, + 7B2CCBA72D11F01F00FE5852 /* VPNAppIntents in Frameworks */, 85DF714624F7FE6100C89288 /* Core.framework in Frameworks */, + 7B2CCBA32D11ABB100FE5852 /* VPNWidgetSupport in Frameworks */, 8512EA4F24ED30D20073EE19 /* WidgetKit.framework in Frameworks */, 4BBBBA872B02E85400D965DA /* DesignResourcesKit in Frameworks */, ); @@ -3788,9 +3794,10 @@ isa = PBXGroup; children = ( 7B1D7A912D0C723B00E48644 /* DesignResourcesKit */, + 31794BFF2821DFB600F18633 /* DuckUI */, 85875B5F29912A2D00115F05 /* SyncUI */, + 7B10FF282D11AA0D00F36BF2 /* VPNiOS */, 37FCAACB2993149A000E420A /* Waitlist */, - 31794BFF2821DFB600F18633 /* DuckUI */, ); path = LocalPackages; sourceTree = ""; @@ -3852,9 +3859,7 @@ 4B5C46282AF2A6DB002A4432 /* Intents */ = { isa = PBXGroup; children = ( - 7B1680FD2D106333005EAE24 /* VPNIntentTunnelController.swift */, - 7B1681042D10BC7B005EAE24 /* VPNAppIntents.swift */, - 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */, + 7B1681042D10BC7B005EAE24 /* VPNIntents.swift */, 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */, ); name = Intents; @@ -4466,6 +4471,7 @@ 7B059F1C2D03A7E400371ED0 /* WidgetsShared.xcassets */, 4BCBE45D2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy */, 853273AC24FEF49600E3C778 /* ColorExtension.swift */, + 7B10FF232D11A56300F36BF2 /* ControlWidgetVPNIntents.swift */, 853273B124FF114700E3C778 /* DeepLinks.swift */, 8512EA5824ED30D30073EE19 /* Info.plist */, 98B001A2251EABB40090EC07 /* InfoPlist.strings */, @@ -4478,7 +4484,7 @@ 853273AF24FEFE4600E3C778 /* WidgetsExtension.entitlements */, 853273A924FEF24300E3C778 /* WidgetViews.swift */, 7B4DC5BF2CB2A4A500EE5CC2 /* VPNStatusValueProvider.swift */, - 7B16810B2D10CF44005EAE24 /* VPNWidgetIntents.swift */, + 7B16810B2D10CF44005EAE24 /* WidgetVPNIntents.swift */, 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */, 7BFC32AF2CB291BB007A8E17 /* VPNControlWidget.swift */, ); @@ -6771,6 +6777,8 @@ 9F8FE9482BAE50E50071E372 /* Lottie */, 9F96F73A2C9144D5009E45D5 /* Onboarding */, 1E5918462CA422A7008ED2B3 /* Navigation */, + 7B2CCBA42D11ABBA00FE5852 /* VPNWidgetSupport */, + 7B2CCBA82D11F02800FE5852 /* VPNAppIntents */, ); productName = DuckDuckGo; productReference = 84E341921E2F7EFB00BDBA6F /* DuckDuckGo.app */; @@ -6820,6 +6828,8 @@ name = WidgetsExtension; packageProductDependencies = ( 4BBBBA862B02E85400D965DA /* DesignResourcesKit */, + 7B2CCBA22D11ABB100FE5852 /* VPNWidgetSupport */, + 7B2CCBA62D11F01F00FE5852 /* VPNAppIntents */, ); productName = WidgetsExtension; productReference = 8512EA4D24ED30D20073EE19 /* WidgetsExtension.appex */; @@ -7643,7 +7653,6 @@ 6FDC64012C92F4A300DB71B3 /* NewTabPageIntroDataStoring.swift in Sources */, 9F46BEF82CD8D7490092E0EF /* OnboardingView+AddToDockContent.swift in Sources */, B609D5522862EAFF0088CAC2 /* InlineWKDownloadDelegate.swift in Sources */, - 7B4DC5BE2CB29D8400EE5CC2 /* VPNToggleIntent.swift in Sources */, BDFF03222BA3D8E200F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */, B652DEFD287BE67400C12A9C /* UserScripts.swift in Sources */, 3768D8472C2CC98C004120AE /* RemoteMessagingConfigMatcherProvider.swift in Sources */, @@ -7855,7 +7864,7 @@ 1EEF12502851016B003DDE57 /* PrivacyIconAndTrackersAnimator.swift in Sources */, 31CB4251273AF50700FA0F3F /* SpeechRecognizerProtocol.swift in Sources */, 319A37172829C8AD0079FBCE /* UITableViewExtension.swift in Sources */, - 7B1681062D10BC96005EAE24 /* VPNAppIntents.swift in Sources */, + 7B1681062D10BC96005EAE24 /* VPNIntents.swift in Sources */, 85EE7F59224673C5000FE757 /* WebContainerNavigationController.swift in Sources */, 6FD1BAE52B87A107000C475C /* AdAttributionReporterStorage.swift in Sources */, 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift in Sources */, @@ -7916,7 +7925,6 @@ 850365F323DE087800D0F787 /* UIImageViewExtension.swift in Sources */, 56D060262C359D2E003BAEB5 /* ContextualOnboardingDialogs.swift in Sources */, 9F8E0F382CCFAA8A001EA7C5 /* AddToDockPromoView.swift in Sources */, - 373608922ABB430D00629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */, C160544129D6044D00B715A1 /* AutofillInterfaceUsernameTruncator.swift in Sources */, 31C70B5528045E3500FB6AD1 /* SecureVaultReporter.swift in Sources */, F4CE6D1B257EA33C00D0A6AA /* FireButtonAnimator.swift in Sources */, @@ -7938,6 +7946,7 @@ 564DE4532C3ED1B700D23241 /* NewTabDaxDialogFactory.swift in Sources */, 1EDE39D22705D4A200C99C72 /* FileSizeDebugViewController.swift in Sources */, 4B412ACC2BBB3D0900A39F5E /* LazyView.swift in Sources */, + 7B10FF252D11A56300F36BF2 /* ControlWidgetVPNIntents.swift in Sources */, 85047C772A0D5D3D00D2FF3F /* SyncSettingsViewController+SyncDelegate.swift in Sources */, 85DDE0402AC6FF65006ABCA2 /* MainView.swift in Sources */, 980891A72237D5D800313A70 /* FeedbackPresenter.swift in Sources */, @@ -7977,7 +7986,6 @@ 3158461A281B08F5004ADB8B /* AutofillLoginListViewModel.swift in Sources */, 31C138A827A3E9C900FFD4B2 /* URLDownloadSession.swift in Sources */, 981FED76220464EF008488D7 /* AutoClearSettingsModel.swift in Sources */, - 7B1680FF2D106333005EAE24 /* VPNIntentTunnelController.swift in Sources */, 83004E882193E8C700DA013C /* TabViewControllerLongPressMenuExtension.swift in Sources */, 98F78B8E22419093007CACF4 /* ThemableNavigationController.swift in Sources */, CBD4F140279EBFB300B20FD7 /* SwiftUICollectionViewCell.swift in Sources */, @@ -8068,6 +8076,7 @@ 1E908BF129827C480008C8F3 /* AutoconsentUserScript.swift in Sources */, 1D200C972BA3157A00108701 /* SettingsNextStepsView.swift in Sources */, 4B0295192537BC6700E00CEF /* ConfigurationDebugViewController.swift in Sources */, + 7B020B9A2D11F99D00876178 /* FavoritesDisplayMode+UserDefaults.swift in Sources */, 7B1C892C2CF714AA0008224E /* VPNTipsModel.swift in Sources */, 1E7A71192934EC6100B7EA19 /* OmniBarNotificationContainerView.swift in Sources */, D60B1F272B9DDE5A00AE4760 /* SubscriptionGoogleView.swift in Sources */, @@ -8154,7 +8163,7 @@ 8590CB67268A2E520089F6BF /* RootDebugViewController.swift in Sources */, 1DEAADEA2BA4539800E25A97 /* SettingsAppearanceView.swift in Sources */, B623C1C22862CA9E0043013E /* DownloadSession.swift in Sources */, - 7B16810C2D10CF44005EAE24 /* VPNWidgetIntents.swift in Sources */, + 7B16810C2D10CF44005EAE24 /* WidgetVPNIntents.swift in Sources */, 9F7CFF7F2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift in Sources */, 985892522260B1B200EEB31B /* ProgressView.swift in Sources */, 85BA585A1F3506AE00C6E8CA /* AppSettings.swift in Sources */, @@ -8426,17 +8435,16 @@ 853273AE24FEF49600E3C778 /* ColorExtension.swift in Sources */, 4BD96E0F2C4DCFEB003BC32C /* VPNSnoozeActivityAttributes.swift in Sources */, 7B1681012D106CB9005EAE24 /* UserTextShared.swift in Sources */, + 7B10FF242D11A56300F36BF2 /* ControlWidgetVPNIntents.swift in Sources */, 373608932ABB432600629E7F /* FavoritesDisplayMode+UserDefaults.swift in Sources */, 4BD96E102C4DF329003BC32C /* VPNSnoozeLiveActivityManager.swift in Sources */, 7B16810A2D10C680005EAE24 /* VPNControlWidget.swift in Sources */, 853273B324FF114700E3C778 /* DeepLinks.swift in Sources */, - 7B4DC5BD2CB29D8400EE5CC2 /* VPNToggleIntent.swift in Sources */, 7B4DC5C22CB2AE4600EE5CC2 /* WidgetKind.swift in Sources */, 853273B424FFB36100E3C778 /* UIColorExtension.swift in Sources */, 853273AB24FEF27500E3C778 /* WidgetViews.swift in Sources */, 7B1681092D10C678005EAE24 /* VPNStatusValueProvider.swift in Sources */, - 7B1681032D106E1D005EAE24 /* VPNIntentTunnelController.swift in Sources */, - 7B16810D2D10CF44005EAE24 /* VPNWidgetIntents.swift in Sources */, + 7B16810D2D10CF44005EAE24 /* WidgetVPNIntents.swift in Sources */, 4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */, 8512EA5424ED30D20073EE19 /* Widgets.swift in Sources */, 85DB12EB2A1FE2A4000A4A72 /* LockScreenWidgets.swift in Sources */, @@ -11522,6 +11530,22 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Common; }; + 7B2CCBA22D11ABB100FE5852 /* VPNWidgetSupport */ = { + isa = XCSwiftPackageProductDependency; + productName = VPNWidgetSupport; + }; + 7B2CCBA42D11ABBA00FE5852 /* VPNWidgetSupport */ = { + isa = XCSwiftPackageProductDependency; + productName = VPNWidgetSupport; + }; + 7B2CCBA62D11F01F00FE5852 /* VPNAppIntents */ = { + isa = XCSwiftPackageProductDependency; + productName = VPNAppIntents; + }; + 7B2CCBA82D11F02800FE5852 /* VPNAppIntents */ = { + isa = XCSwiftPackageProductDependency; + productName = VPNAppIntents; + }; 851481872A600EFC00ABC65F /* RemoteMessaging */ = { isa = XCSwiftPackageProductDependency; package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo/VPNAutoShortcuts.swift b/DuckDuckGo/VPNAutoShortcuts.swift index 05fd7479e2..6d2a916c61 100644 --- a/DuckDuckGo/VPNAutoShortcuts.swift +++ b/DuckDuckGo/VPNAutoShortcuts.swift @@ -19,13 +19,21 @@ import AppIntents import Foundation +import VPNAppIntents +/* +@available(iOS 17.0, *) +public struct VPNAppIntents: AppIntentsPackage { + public static var includedPackages: [any AppIntentsPackage.Type] { + [VPNAppIntents.self] + } +}*/ @available(iOS 17.0, *) struct VPNAutoShortcutsiOS17: AppShortcutsProvider { @AppShortcutsBuilder static var appShortcuts: [AppShortcut] { - AppShortcut(intent: EnableVPNAppIntent(), + AppShortcut(intent: EnableVPNIntent(), phrases: [ "Connect \(.applicationName) VPN", "Connect the \(.applicationName) VPN", @@ -42,7 +50,7 @@ struct VPNAutoShortcutsiOS17: AppShortcutsProvider { "Protect my connection with \(.applicationName)" ], systemImageName: "globe") - AppShortcut(intent: DisableVPNAppIntent(), + AppShortcut(intent: DisableVPNIntent(), phrases: [ "Disconnect \(.applicationName) VPN", "Disconnect the \(.applicationName) VPN", diff --git a/DuckDuckGo/VPNIntentTunnelController.swift b/DuckDuckGo/VPNIntentTunnelController.swift deleted file mode 100644 index 03884adfe9..0000000000 --- a/DuckDuckGo/VPNIntentTunnelController.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// VPNIntentTunnelController.swift -// DuckDuckGo -// -// 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 Core -import NetworkExtension - -@available(iOS 17.0, *) -struct VPNIntentTunnelController { - - enum StartFailure: CustomNSError { - case vpnNotConfigured - } - - enum StopFailure: CustomNSError { - case vpnNotConfigured - } - - func start() async throws { - let managers = try await NETunnelProviderManager.loadAllFromPreferences() - guard let manager = managers.first else { - throw StartFailure.vpnNotConfigured - } - - manager.isOnDemandEnabled = true - try await manager.saveToPreferences() - try manager.connection.startVPNTunnel() - - await VPNSnoozeLiveActivityManager().endSnoozeActivity() - - VPNReloadStatusWidgets() - } - - func stop() async throws { - let managers = try await NETunnelProviderManager.loadAllFromPreferences() - guard let manager = managers.first else { - throw StopFailure.vpnNotConfigured - } - - manager.isOnDemandEnabled = false - try await manager.saveToPreferences() - manager.connection.stopVPNTunnel() - - await VPNSnoozeLiveActivityManager().endSnoozeActivity() - - VPNReloadStatusWidgets() - } -} diff --git a/DuckDuckGo/VPNAppIntents.swift b/DuckDuckGo/VPNIntents.swift similarity index 84% rename from DuckDuckGo/VPNAppIntents.swift rename to DuckDuckGo/VPNIntents.swift index 9d31388256..d68319b8ab 100644 --- a/DuckDuckGo/VPNAppIntents.swift +++ b/DuckDuckGo/VPNIntents.swift @@ -22,17 +22,18 @@ import NetworkExtension import NetworkProtection import WidgetKit import Core +import VPNWidgetSupport // MARK: - Enable & Disable /// App intent to disable the VPN /// /// This is used in App Shortcuts, for things like Shortcuts.app, Spotlight and Siri. -/// This is very similar to ``WidgetVPNDisableIntent``, but this runs in-app, allows continuation in the app if needed, +/// This is very similar to ``WidgetDisableVPNIntent``, but this runs in-app, allows continuation in the app if needed, /// and provides a result dialog. /// @available(iOS 17.0, *) -struct DisableVPNAppIntent: AppIntent { +struct DisableVPNIntent: AppIntent { private enum DisableAttemptFailure: CustomNSError { case cancelled @@ -49,12 +50,15 @@ struct DisableVPNAppIntent: AppIntent { do { DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectAttempt) - let controller = VPNIntentTunnelController() + let controller = VPNWidgetTunnelController() try await controller.stop() + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectSuccess) return .result(dialog: "DuckDuckGo VPN is disconnecting...") - } catch VPNIntentTunnelController.StopFailure.vpnNotConfigured { + } catch VPNWidgetTunnelController.StopFailure.vpnNotConfigured { DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectCancelled) return .result(dialog: "The DuckDuckGo VPN is not connected") } catch { @@ -67,12 +71,12 @@ struct DisableVPNAppIntent: AppIntent { /// App intent to enable the VPN /// /// This is used in App Shortcuts, for things like Shortcuts.app, Spotlight and Siri. -/// This is very similar to ``VPNWidgetEnableIntent``, but this runs in-app, allows continuation in the app if needed, +/// This is very similar to ``WidgetEnableVPNIntent``, but this runs in-app, allows continuation in the app if needed, /// and provides a result dialog. /// @available(iOS 17.0, *) @available(iOSApplicationExtension, unavailable) -struct EnableVPNAppIntent: ForegroundContinuableIntent { +struct EnableVPNIntent: ForegroundContinuableIntent { static let title: LocalizedStringResource = "Enable DuckDuckGo VPN" static let description: LocalizedStringResource = "Enables the DuckDuckGo VPN" static let openAppWhenRun: Bool = false @@ -84,14 +88,17 @@ struct EnableVPNAppIntent: ForegroundContinuableIntent { do { DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectAttempt) - let controller = VPNIntentTunnelController() + let controller = VPNWidgetTunnelController() try await controller.start() + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectSuccess) return .result(dialog: "DuckDuckGo VPN is connecting...") } catch { switch error { - case VPNIntentTunnelController.StartFailure.vpnNotConfigured: + case VPNWidgetTunnelController.StartFailure.vpnNotConfigured: DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) let dialog = IntentDialog(stringLiteral: UserText.vpnNeedsToBeEnabledFromApp) diff --git a/DuckDuckGo/VPNSnoozeActivityAttributes.swift b/DuckDuckGo/VPNSnoozeActivityAttributes.swift index b5155f2ea8..772aad8a8e 100644 --- a/DuckDuckGo/VPNSnoozeActivityAttributes.swift +++ b/DuckDuckGo/VPNSnoozeActivityAttributes.swift @@ -19,7 +19,6 @@ import Foundation import ActivityKit -import SwiftUI struct VPNSnoozeActivityAttributes: ActivityAttributes { struct ContentState: Codable & Hashable { diff --git a/LocalPackages/VPNiOS/.gitignore b/LocalPackages/VPNiOS/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/LocalPackages/VPNiOS/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/LocalPackages/VPNiOS/Package.swift b/LocalPackages/VPNiOS/Package.swift new file mode 100644 index 0000000000..691eaf6434 --- /dev/null +++ b/LocalPackages/VPNiOS/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "VPNiOS", + platforms: [ + .iOS(.v15) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "VPNWidgetSupport", + targets: ["VPNWidgetSupport"]), + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "VPNAppIntents", + targets: ["VPNAppIntents"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "VPNWidgetSupport" + ), + .target( + name: "VPNAppIntents", + dependencies: ["VPNWidgetSupport"] + ), + ] +) diff --git a/LocalPackages/VPNiOS/Sources/VPNAppIntents/VPNAppIntents.swift b/LocalPackages/VPNiOS/Sources/VPNAppIntents/VPNAppIntents.swift new file mode 100644 index 0000000000..936d09f160 --- /dev/null +++ b/LocalPackages/VPNiOS/Sources/VPNAppIntents/VPNAppIntents.swift @@ -0,0 +1,92 @@ +// +// VPNAppIntents.swift +// VPNiOS +// +// Created by ddg on 12/17/24. +// + +// MARK: - Toggle + +/// `ForegroundContinuableIntent` isn't available for extensions, which makes it impossible to call +/// from extensions. This is the recommended workaround from: +/// https://mastodon.social/@mgorbach/110812347476671807 +/// + +import AppIntents +import VPNWidgetSupport +import WidgetKit + +public struct VPNAppIntents: AppIntentsPackage { } +/* +@available(iOS 17.0, *) +public struct ControlWidgetToggleVPNIntent: SetValueIntent { + public static let title: LocalizedStringResource = "Toggle DuckDuckGo VPN from the Control Center Widget" + public static let description: LocalizedStringResource = "Toggles the DuckDuckGo VPN from the Control Center widget" + public static let isDiscoverable = false + public static let openAppWhenRun = false + + @Parameter(title: "Enabled") + public var value: Bool + + public init() {} + + public func perform() async throws -> some IntentResult { + if value { + try await startVPN() + } else { + try await stopVPN() + } + + return .result() + } + + private func startVPN() async throws { + do { + //DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectAttempt) + + let controller = VPNWidgetTunnelController() + try await controller.start() + + WidgetCenter.shared.reloadAllTimelines() + + //await VPNSnoozeLiveActivityManager().endSnoozeActivity() + //VPNReloadStatusWidgets() + + //DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectSuccess) + } catch { + switch error { + case VPNWidgetTunnelController.StartFailure.vpnNotConfigured: + //DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectCancelled) + throw error + default: + //DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectFailure, error: error) + throw error + } + } + } + + private func stopVPN() async throws { + do { + //DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterDisconnectAttempt) + + let controller = VPNWidgetTunnelController() + try await controller.stop() + + WidgetCenter.shared.reloadAllTimelines() + //await VPNSnoozeLiveActivityManager().endSnoozeActivity() + //VPNReloadStatusWidgets() + + //DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterDisconnectSuccess) + } catch { + switch error { + case VPNWidgetTunnelController.StopFailure.vpnNotConfigured: + //DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterDisconnectCancelled) + throw error + default: + //DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterDisconnectFailure, error: error) + throw error + } + } + } +} +*/ diff --git a/LocalPackages/VPNiOS/Sources/VPNWidgetSupport/VPNWidgetTunnelController.swift b/LocalPackages/VPNiOS/Sources/VPNWidgetSupport/VPNWidgetTunnelController.swift new file mode 100644 index 0000000000..f95644af83 --- /dev/null +++ b/LocalPackages/VPNiOS/Sources/VPNWidgetSupport/VPNWidgetTunnelController.swift @@ -0,0 +1,96 @@ +// +// VPNIntentTunnelController.swift +// DuckDuckGo +// +// 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 NetworkExtension + +@available(iOS 17.0, *) +public struct VPNWidgetTunnelController: Sendable { + + public enum StartFailure: CustomNSError { + case vpnNotConfigured + } + + public enum StopFailure: CustomNSError { + case vpnNotConfigured + } + + public init() {} + + public var status: NEVPNStatus { + get async { + guard let manager = try? await NETunnelProviderManager.loadAllFromPreferences().first else { + return .invalid + } + + return manager.connection.status + } + } + + public func start() async throws { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + guard let manager = managers.first else { + throw StartFailure.vpnNotConfigured + } + + manager.isOnDemandEnabled = true + try await manager.saveToPreferences() + try manager.connection.startVPNTunnel() + + try await awaitUntilStatusIsNoLongerTransitioning(manager: manager) + } + + public func stop() async throws { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + guard let manager = managers.first else { + throw StopFailure.vpnNotConfigured + } + + manager.isOnDemandEnabled = false + try await manager.saveToPreferences() + manager.connection.stopVPNTunnel() + + try await awaitUntilStatusIsNoLongerTransitioning(manager: manager) + } + + private func awaitUntilStatusIsNoLongerTransitioning(manager: NETunnelProviderManager) async throws { + + while true { + try await Task.sleep(for: .milliseconds(500)) + + if manager.connection.status != .connecting + && manager.connection.status != .disconnecting { + + break + } + } +/* + for await notification in NotificationCenter.default.notifications(named: NSNotification.Name.NEVPNStatusDidChange) { + + try Task.checkCancellation() + + /// If there's no connection in the notification, or the connection status is no longer + /// `connecting` we just bail out here. + guard let connection = notification.object as? NEVPNConnection, + connection.status == .connecting || connection.status == .disconnecting else { + + break + } + }*/ + } +} diff --git a/DuckDuckGo/VPNToggleIntent.swift b/Widgets/ControlWidgetVPNIntents.swift similarity index 78% rename from DuckDuckGo/VPNToggleIntent.swift rename to Widgets/ControlWidgetVPNIntents.swift index c44a76dd66..11d9cfb366 100644 --- a/DuckDuckGo/VPNToggleIntent.swift +++ b/Widgets/ControlWidgetVPNIntents.swift @@ -23,6 +23,8 @@ import NetworkProtection import WidgetKit import Core import OSLog +import VPNWidgetSupport +import VPNAppIntents // MARK: - Toggle @@ -31,10 +33,10 @@ import OSLog /// https://mastodon.social/@mgorbach/110812347476671807 /// @available(iOS 17.0, *) -struct VPNToggleIntent: SetValueIntent { - static let title: LocalizedStringResource = "Toggle DuckDuckGo VPN" - static let description: LocalizedStringResource = "Toggles the DuckDuckGo VPN" - static let isDiscoverable: Bool = false +struct ControlWidgetToggleVPNIntent: SetValueIntent { + static let title: LocalizedStringResource = "Toggle DuckDuckGo VPN from the Control Center Widget" + static let description: LocalizedStringResource = "Toggles the DuckDuckGo VPN from the Control Center widget" + static let isDiscoverable = false @Parameter(title: "Enabled") var value: Bool @@ -43,25 +45,28 @@ struct VPNToggleIntent: SetValueIntent { func perform() async throws -> some IntentResult { if value { try await startVPN() - return .result() } else { try await stopVPN() - return .result() } + + return .result() } private func startVPN() async throws { do { DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectAttempt) - let controller = VPNIntentTunnelController() + let controller = VPNWidgetTunnelController() try await controller.start() + + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() + DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectSuccess) } catch { switch error { - case VPNIntentTunnelController.StartFailure.vpnNotConfigured: + case VPNWidgetTunnelController.StartFailure.vpnNotConfigured: DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectCancelled) - throw error default: DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectFailure, error: error) @@ -74,12 +79,16 @@ struct VPNToggleIntent: SetValueIntent { do { DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterDisconnectAttempt) - let controller = VPNIntentTunnelController() + let controller = VPNWidgetTunnelController() try await controller.stop() + + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() + DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterDisconnectSuccess) } catch { switch error { - case VPNIntentTunnelController.StopFailure.vpnNotConfigured: + case VPNWidgetTunnelController.StopFailure.vpnNotConfigured: DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterDisconnectCancelled) throw error default: diff --git a/Widgets/VPNControlWidget.swift b/Widgets/VPNControlWidget.swift index 4495e41897..d016a061b3 100644 --- a/Widgets/VPNControlWidget.swift +++ b/Widgets/VPNControlWidget.swift @@ -19,11 +19,12 @@ import Foundation import SwiftUI +import VPNAppIntents import WidgetKit @available(iOSApplicationExtension 18.0, *) public struct VPNControlWidget: ControlWidget { - static let displayName = LocalizedStringResource(stringLiteral: "VPN") + static let displayName = LocalizedStringResource(stringLiteral: "DuckDuckGo VPN") static let description = LocalizedStringResource(stringLiteral: "View and manage your VPN connection. Requires a Privacy Pro subscription.") public init() {} @@ -32,7 +33,7 @@ public struct VPNControlWidget: ControlWidget { StaticControlConfiguration(kind: .vpn, provider: VPNControlStatusValueProvider()) { status in - ControlWidgetToggle("DuckDuckGo VPN", isOn: status.isConnected, action: VPNToggleIntent()) { isOn in + ControlWidgetToggle("DuckDuckGo VPN", isOn: status.isConnected, action: ControlWidgetToggleVPNIntent()) { isOn in if isOn { Label("Enabled", image: "ControlCenter-VPN-on") } else { diff --git a/Widgets/VPNStatusValueProvider.swift b/Widgets/VPNStatusValueProvider.swift index 1da8a1f9a1..b5d3ba1c7a 100644 --- a/Widgets/VPNStatusValueProvider.swift +++ b/Widgets/VPNStatusValueProvider.swift @@ -27,6 +27,7 @@ struct VPNControlStatusValueProvider: ControlValueProvider { func currentValue() async throws -> VPNStatus { guard let manager = try await NETunnelProviderManager.loadAllFromPreferences().first else { + return .notConfigured } diff --git a/Widgets/VPNWidget.swift b/Widgets/VPNWidget.swift index a2f5af1ab4..35dc033842 100644 --- a/Widgets/VPNWidget.swift +++ b/Widgets/VPNWidget.swift @@ -174,7 +174,7 @@ struct VPNStatusView: View { switch status { case .connected: let buttonTitle = snoozeTimingStore.isSnoozing ? UserText.vpnWidgetLiveActivityWakeUpButton : UserText.vpnWidgetDisconnectButton - let intent: any AppIntent = snoozeTimingStore.isSnoozing ? CancelSnoozeVPNIntent() : VPNWidgetDisableIntent() + let intent: any AppIntent = snoozeTimingStore.isSnoozing ? CancelSnoozeVPNIntent() : WidgetDisableVPNIntent() Button(buttonTitle, intent: intent) .font(.system(size: 14, weight: .semibold)) @@ -192,7 +192,7 @@ struct VPNStatusView: View { .padding(.top, 6) .padding(.bottom, 16) case .connecting, .reasserting: - Button(UserText.vpnWidgetDisconnectButton, intent: VPNWidgetDisableIntent()) + Button(UserText.vpnWidgetDisconnectButton, intent: WidgetDisableVPNIntent()) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(disconnectButtonForegroundColor(isDisabled: status != .connected)) .buttonStyle(.borderedProminent) @@ -234,7 +234,7 @@ struct VPNStatusView: View { private var connectButton: Button { switch entry.status { case .status: - Button(UserText.vpnWidgetConnectButton, intent: VPNWidgetEnableIntent()) + Button(UserText.vpnWidgetConnectButton, intent: WidgetEnableVPNIntent()) case .error, .notConfigured: Button(UserText.vpnWidgetConnectButton) { openURL(DeepLinks.openVPN) diff --git a/Widgets/WidgetKind.swift b/Widgets/WidgetKind.swift index 42c748f139..2c388c5855 100644 --- a/Widgets/WidgetKind.swift +++ b/Widgets/WidgetKind.swift @@ -46,8 +46,8 @@ extension ControlCenter { extension StaticControlConfiguration { @MainActor @preconcurrency init(kind: ControlWidgetKind, - provider: Provider, @ControlWidgetTemplateBuilder - content: @escaping (Provider.Value) -> Content) + provider: Provider, + @ControlWidgetTemplateBuilder content: @escaping (Provider.Value) -> Content) where Provider: ControlValueProvider { self.init(kind: kind.rawValue, provider: provider, content: content) } diff --git a/Widgets/VPNWidgetIntents.swift b/Widgets/WidgetVPNIntents.swift similarity index 88% rename from Widgets/VPNWidgetIntents.swift rename to Widgets/WidgetVPNIntents.swift index 70d77eb270..32609cd25c 100644 --- a/Widgets/VPNWidgetIntents.swift +++ b/Widgets/WidgetVPNIntents.swift @@ -22,6 +22,7 @@ import NetworkExtension import NetworkProtection import WidgetKit import Core +import VPNWidgetSupport // MARK: - Enable & Disable @@ -32,7 +33,7 @@ import Core /// does not support continuation in the app and does not provide any result dialog. /// @available(iOS 17.0, *) -struct VPNWidgetDisableIntent: AppIntent { +struct WidgetDisableVPNIntent: AppIntent { private enum DisableAttemptFailure: CustomNSError { case cancelled @@ -41,7 +42,7 @@ struct VPNWidgetDisableIntent: AppIntent { static let title: LocalizedStringResource = "Disable DuckDuckGo VPN" static let description: LocalizedStringResource = "Disables the DuckDuckGo VPN" static let openAppWhenRun: Bool = false - static let isDiscoverable: Bool = true + static let isDiscoverable: Bool = false static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication @MainActor @@ -49,12 +50,15 @@ struct VPNWidgetDisableIntent: AppIntent { do { DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectAttempt) - let controller = VPNIntentTunnelController() + let controller = VPNWidgetTunnelController() try await controller.stop() + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectSuccess) return .result() - } catch VPNIntentTunnelController.StopFailure.vpnNotConfigured { + } catch VPNWidgetTunnelController.StopFailure.vpnNotConfigured { DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectCancelled) return .result() } catch { @@ -71,11 +75,11 @@ struct VPNWidgetDisableIntent: AppIntent { /// does not support continuation in the app and does not provide any result dialog. /// @available(iOS 17.0, *) -struct VPNWidgetEnableIntent: AppIntent { +struct WidgetEnableVPNIntent: AppIntent { static let title: LocalizedStringResource = "Enable DuckDuckGo VPN" static let description: LocalizedStringResource = "Enables the DuckDuckGo VPN" static let openAppWhenRun: Bool = false - static let isDiscoverable: Bool = true + static let isDiscoverable: Bool = false static var authenticationPolicy: IntentAuthenticationPolicy = .alwaysAllowed @MainActor @@ -83,14 +87,17 @@ struct VPNWidgetEnableIntent: AppIntent { do { DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectAttempt) - let controller = VPNIntentTunnelController() + let controller = VPNWidgetTunnelController() try await controller.start() + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectSuccess) return .result() } catch { switch error { - case VPNIntentTunnelController.StartFailure.vpnNotConfigured: + case VPNWidgetTunnelController.StartFailure.vpnNotConfigured: DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) throw error default: @@ -128,7 +135,6 @@ struct CancelSnoozeVPNIntent: AppIntent { return .result() } } - } @available(iOS 17.0, *) diff --git a/Widgets/Widgets.swift b/Widgets/Widgets.swift index 8cc9844e41..e1f0cfccbb 100644 --- a/Widgets/Widgets.swift +++ b/Widgets/Widgets.swift @@ -17,6 +17,7 @@ // limitations under the License. // +import AppIntents import Common import WidgetKit import SwiftUI @@ -27,6 +28,14 @@ import Bookmarks import Persistence import NetworkExtension import os.log +import VPNAppIntents + +@available(iOS 17.0, *) +public struct WidgetsExtension: AppIntentsPackage { + public static var includedPackages: [any AppIntentsPackage.Type] { + [VPNAppIntents.self] + } +} struct Favorite { diff --git a/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/Contents.json b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/Contents.json index 87a30f4e23..ca0236fb00 100644 --- a/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/Contents.json +++ b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/Contents.json @@ -3,12 +3,9 @@ "author" : "xcode", "version" : 1 }, - "properties" : { - "symbol-rendering-intent" : "template" - }, "symbols" : [ { - "filename" : "VPN-Off.svg", + "filename" : "VPNOFF4.svg", "idiom" : "universal" } ] diff --git a/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/VPN-Off.svg b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/VPN-Off.svg deleted file mode 100644 index 489c1ead02..0000000000 --- a/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/VPN-Off.svg +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from circle - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/VPNOFF4.svg b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/VPNOFF4.svg new file mode 100644 index 0000000000..123cf8d738 --- /dev/null +++ b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/VPNOFF4.svg @@ -0,0 +1,119 @@ + + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.6.0 + Requires Xcode 16 or greater + Generated from trash.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/Contents.json b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/Contents.json index 3cf25de919..7bc2896a16 100644 --- a/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/Contents.json +++ b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/Contents.json @@ -3,12 +3,9 @@ "author" : "xcode", "version" : 1 }, - "properties" : { - "symbol-rendering-intent" : "template" - }, "symbols" : [ { - "filename" : "VPN-On.svg", + "filename" : "VPNON4.svg", "idiom" : "universal" } ] diff --git a/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/VPN-On.svg b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/VPN-On.svg deleted file mode 100644 index 253c613363..0000000000 --- a/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/VPN-On.svg +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from circle - Typeset at 100.0 points - Small - Medium - Large - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from circle - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/VPNON4.svg b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/VPNON4.svg new file mode 100644 index 0000000000..63d2a85180 --- /dev/null +++ b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/VPNON4.svg @@ -0,0 +1,119 @@ + + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.6.0 + Requires Xcode 16 or greater + Generated from trash.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +