diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index ef185fb0b6..e2781679c6 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -463,8 +463,22 @@ extension Pixel { case networkProtectionWidgetConnectAttempt case networkProtectionWidgetConnectSuccess + case networkProtectionWidgetConnectCancelled + case networkProtectionWidgetConnectFailure case networkProtectionWidgetDisconnectAttempt case networkProtectionWidgetDisconnectSuccess + case networkProtectionWidgetDisconnectCancelled + case networkProtectionWidgetDisconnectFailure + + case vpnControlCenterConnectAttempt + case vpnControlCenterConnectSuccess + case vpnControlCenterConnectCancelled + case vpnControlCenterConnectFailure + + case vpnControlCenterDisconnectAttempt + case vpnControlCenterDisconnectSuccess + case vpnControlCenterDisconnectCancelled + case vpnControlCenterDisconnectFailure case networkProtectionDNSUpdateCustom case networkProtectionDNSUpdateDefault @@ -1660,8 +1674,22 @@ 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" + + 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" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index fb60d487f5..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 */; }; @@ -234,8 +233,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 */; }; - 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 */; }; @@ -368,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 */; }; @@ -375,16 +373,27 @@ 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 */; }; + 7B1681012D106CB9005EAE24 /* UserTextShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1681002D106CB4005EAE24 /* UserTextShared.swift */; }; + 7B1681022D106CCC005EAE24 /* UserTextShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1681002D106CB4005EAE24 /* UserTextShared.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 /* 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 */; }; - 7B4DC5C02CB2A4A500EE5CC2 /* VPNStatusValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5BF2CB2A4A500EE5CC2 /* VPNStatusValueProvider.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 */; }; + 7B4DC5E22CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */; }; 7B4F87E72D0734090010B18F /* ControlCenterWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4F87E62D0734060010B18F /* ControlCenterWidget.swift */; }; 7B4F87EA2D0738F90010B18F /* SiriEducationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4F87E92D0738F40010B18F /* SiriEducationView.swift */; }; 7B4F87EC2D07396A0010B18F /* SiriEducation.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B4F87EB2D07396A0010B18F /* SiriEducation.xcassets */; }; @@ -394,7 +403,6 @@ 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 */; }; @@ -1334,6 +1342,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; @@ -1591,7 +1609,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 = ""; }; @@ -1722,14 +1739,19 @@ 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 = ""; }; + 7B1681002D106CB4005EAE24 /* UserTextShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTextShared.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 = ""; }; 7B4F87E62D0734060010B18F /* ControlCenterWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlCenterWidget.swift; sourceTree = ""; }; 7B4F87E92D0738F40010B18F /* SiriEducationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiriEducationView.swift; sourceTree = ""; }; 7B4F87EB2D07396A0010B18F /* SiriEducation.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = SiriEducation.xcassets; sourceTree = ""; }; @@ -3134,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 */, @@ -3168,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 */, ); @@ -3768,9 +3794,10 @@ isa = PBXGroup; children = ( 7B1D7A912D0C723B00E48644 /* DesignResourcesKit */, + 31794BFF2821DFB600F18633 /* DuckUI */, 85875B5F29912A2D00115F05 /* SyncUI */, + 7B10FF282D11AA0D00F36BF2 /* VPNiOS */, 37FCAACB2993149A000E420A /* Waitlist */, - 31794BFF2821DFB600F18633 /* DuckUI */, ); path = LocalPackages; sourceTree = ""; @@ -3832,8 +3859,8 @@ 4B5C46282AF2A6DB002A4432 /* Intents */ = { isa = PBXGroup; children = ( - 4B5C46292AF2A6E6002A4432 /* VPNIntents.swift */, - 7B4DC5BC2CB29D8400EE5CC2 /* VPNToggleIntent.swift */, + 7B1681042D10BC7B005EAE24 /* VPNIntents.swift */, + 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */, ); name = Intents; sourceTree = ""; @@ -4444,17 +4471,20 @@ 7B059F1C2D03A7E400371ED0 /* WidgetsShared.xcassets */, 4BCBE45D2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy */, 853273AC24FEF49600E3C778 /* ColorExtension.swift */, + 7B10FF232D11A56300F36BF2 /* ControlWidgetVPNIntents.swift */, 853273B124FF114700E3C778 /* DeepLinks.swift */, 8512EA5824ED30D30073EE19 /* Info.plist */, 98B001A2251EABB40090EC07 /* InfoPlist.strings */, 98B001A8251EABB40090EC07 /* Localizable.strings */, 85DB12EA2A1FE2A4000A4A72 /* LockScreenWidgets.swift */, 8544C37A250B823600A0FE73 /* UserText.swift */, + 7B1681002D106CB4005EAE24 /* UserTextShared.swift */, 7B4DC5C12CB2AE4600EE5CC2 /* WidgetKind.swift */, 8512EA5324ED30D20073EE19 /* Widgets.swift */, 853273AF24FEFE4600E3C778 /* WidgetsExtension.entitlements */, 853273A924FEF24300E3C778 /* WidgetViews.swift */, 7B4DC5BF2CB2A4A500EE5CC2 /* VPNStatusValueProvider.swift */, + 7B16810B2D10CF44005EAE24 /* WidgetVPNIntents.swift */, 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */, 7BFC32AF2CB291BB007A8E17 /* VPNControlWidget.swift */, ); @@ -6721,6 +6751,7 @@ F10307651E7D5B2C0059FEC7 /* Copy Frameworks */, 83E282AC20BC1840005FBE88 /* Embed App Extensions */, EE9286812A812BD2002B7818 /* Embed PacketTunnelProvider */, + 7B4DC5D92CB2D0F100EE5CC2 /* Embed ExtensionKit Extensions */, ); buildRules = ( ); @@ -6746,6 +6777,8 @@ 9F8FE9482BAE50E50071E372 /* Lottie */, 9F96F73A2C9144D5009E45D5 /* Onboarding */, 1E5918462CA422A7008ED2B3 /* Navigation */, + 7B2CCBA42D11ABBA00FE5852 /* VPNWidgetSupport */, + 7B2CCBA82D11F02800FE5852 /* VPNAppIntents */, ); productName = DuckDuckGo; productReference = 84E341921E2F7EFB00BDBA6F /* DuckDuckGo.app */; @@ -6795,6 +6828,8 @@ name = WidgetsExtension; packageProductDependencies = ( 4BBBBA862B02E85400D965DA /* DesignResourcesKit */, + 7B2CCBA22D11ABB100FE5852 /* VPNWidgetSupport */, + 7B2CCBA62D11F01F00FE5852 /* VPNAppIntents */, ); productName = WidgetsExtension; productReference = 8512EA4D24ED30D20073EE19 /* WidgetsExtension.appex */; @@ -7600,6 +7635,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 */, @@ -7617,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 */, @@ -7721,6 +7756,7 @@ D6D95CE32B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift in Sources */, C185ED612BD4329700BAE9DC /* ImportPasswordsStatusHandler.swift in Sources */, F4D9C4FA25117A0F00814B71 /* HomeMessageStorage.swift in Sources */, + 7B4DC5E22CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift in Sources */, 6F5041C92CC11A5100989E48 /* SimpleNewTabPageView.swift in Sources */, D69FBF762B28BE3600B505F1 /* SettingsSubscriptionView.swift in Sources */, D664C7CC2B289AA200CBFA76 /* SubscriptionPagesUserScript.swift in Sources */, @@ -7828,6 +7864,7 @@ 1EEF12502851016B003DDE57 /* PrivacyIconAndTrackersAnimator.swift in Sources */, 31CB4251273AF50700FA0F3F /* SpeechRecognizerProtocol.swift in Sources */, 319A37172829C8AD0079FBCE /* UITableViewExtension.swift in Sources */, + 7B1681062D10BC96005EAE24 /* VPNIntents.swift in Sources */, 85EE7F59224673C5000FE757 /* WebContainerNavigationController.swift in Sources */, 6FD1BAE52B87A107000C475C /* AdAttributionReporterStorage.swift in Sources */, 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift in Sources */, @@ -7888,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 */, @@ -7910,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 */, @@ -8039,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 */, @@ -8055,7 +8093,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 */, @@ -8126,6 +8163,7 @@ 8590CB67268A2E520089F6BF /* RootDebugViewController.swift in Sources */, 1DEAADEA2BA4539800E25A97 /* SettingsAppearanceView.swift in Sources */, B623C1C22862CA9E0043013E /* DownloadSession.swift in Sources */, + 7B16810C2D10CF44005EAE24 /* WidgetVPNIntents.swift in Sources */, 9F7CFF7F2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift in Sources */, 985892522260B1B200EEB31B /* ProgressView.swift in Sources */, 85BA585A1F3506AE00C6E8CA /* AppSettings.swift in Sources */, @@ -8396,16 +8434,17 @@ files = ( 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 */, - 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 */, - 4B5C462B2AF2BDC4002A4432 /* VPNIntents.swift in Sources */, + 7B1681092D10C678005EAE24 /* VPNStatusValueProvider.swift in Sources */, + 7B16810D2D10CF44005EAE24 /* WidgetVPNIntents.swift in Sources */, 4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */, 8512EA5424ED30D20073EE19 /* Widgets.swift in Sources */, 85DB12EB2A1FE2A4000A4A72 /* LockScreenWidgets.swift in Sources */, @@ -11491,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/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/Info.plist b/DuckDuckGo/Info.plist index 6d3941b0ca..9fbb5418be 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,15 @@ 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 + SUBSCRIPTION_APP_GROUP + $(AppIdentifierPrefix)$(SUBSCRIPTION_APP_GROUP) UIApplicationShortcutItems @@ -227,10 +234,6 @@ UIStatusBarHidden - SUBSCRIPTION_APP_GROUP - $(AppIdentifierPrefix)$(SUBSCRIPTION_APP_GROUP) - NSSupportsLiveActivities - UIStatusBarStyle UIStatusBarStyleDefault UISupportedInterfaceOrientations~ipad @@ -240,8 +243,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSAdvertisingAttributionReportEndpoint - https://duckduckgo.com UIViewControllerBasedStatusBarAppearance diff --git a/DuckDuckGo/VPNAutoShortcuts.swift b/DuckDuckGo/VPNAutoShortcuts.swift new file mode 100644 index 0000000000..6d2a916c61 --- /dev/null +++ b/DuckDuckGo/VPNAutoShortcuts.swift @@ -0,0 +1,69 @@ +// +// 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 +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: EnableVPNIntent(), + 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: [ + "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 e064b99765..d68319b8ab 100644 --- a/DuckDuckGo/VPNIntents.swift +++ b/DuckDuckGo/VPNIntents.swift @@ -1,8 +1,8 @@ // -// VPNIntents.swift +// VPNSiriIntents.swift // DuckDuckGo // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// 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. @@ -22,150 +22,94 @@ 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 ``WidgetDisableVPNIntent``, but this runs in-app, allows continuation in the app if needed, +/// and provides a result dialog. +/// @available(iOS 17.0, *) struct DisableVPNIntent: AppIntent { - static let title: LocalizedStringResource = "Disable VPN" + 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 = false + static let isDiscoverable: Bool = true + static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication @MainActor - func perform() async throws -> some IntentResult { + func perform() async throws -> some IntentResult & ProvidesDialog { do { - DailyPixel.fire(pixel: .networkProtectionWidgetDisconnectAttempt) - - let managers = try await NETunnelProviderManager.loadAllFromPreferences() - guard let manager = managers.first else { - return .result() - } + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectAttempt) - manager.isOnDemandEnabled = false - try await manager.saveToPreferences() - manager.connection.stopVPNTunnel() + let controller = VPNWidgetTunnelController() + try await controller.stop() 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() - return .result() + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectSuccess) + return .result(dialog: "DuckDuckGo VPN is disconnecting...") + } catch VPNWidgetTunnelController.StopFailure.vpnNotConfigured { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectCancelled) + return .result(dialog: "The DuckDuckGo VPN is not connected") } catch { - return .result() + 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 ``WidgetEnableVPNIntent``, but this runs in-app, allows continuation in the app if needed, +/// and provides a result dialog. +/// @available(iOS 17.0, *) -struct EnableVPNIntent: AppIntent { - - static let title: LocalizedStringResource = "Enable VPN" +@available(iOSApplicationExtension, unavailable) +struct EnableVPNIntent: 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 = false + static let isDiscoverable: Bool = true + static var authenticationPolicy: IntentAuthenticationPolicy = .alwaysAllowed @MainActor - func perform() async throws -> some IntentResult { + func perform() async throws -> some IntentResult & ProvidesDialog { do { - DailyPixel.fire(pixel: .networkProtectionWidgetConnectAttempt) - - let managers = try await NETunnelProviderManager.loadAllFromPreferences() - guard let manager = managers.first else { - return .result() - } + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectAttempt) - manager.isOnDemandEnabled = true - try await manager.saveToPreferences() - try manager.connection.startVPNTunnel() + let controller = VPNWidgetTunnelController() + try await controller.start() await VPNSnoozeLiveActivityManager().endSnoozeActivity() - - var iterations = 0 - - while iterations <= 10 { - try? await Task.sleep(interval: .seconds(0.5)) - - if manager.connection.status == .connected { - DailyPixel.fire(pixel: .networkProtectionWidgetConnectSuccess) - return .result() - } - - iterations += 1 - } - VPNReloadStatusWidgets() - return .result() + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectSuccess) + return .result(dialog: "DuckDuckGo VPN is connecting...") } catch { - return .result() - } - } + switch error { + case VPNWidgetTunnelController.StartFailure.vpnNotConfigured: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) -} - -// MARK: - Snooze - -@available(iOS 17.0, *) -struct CancelSnoozeVPNIntent: AppIntent { - - static let title: LocalizedStringResource = "Resume VPN" - static let description: LocalizedStringResource = "Resumes the DuckDuckGo VPN" - static let openAppWhenRun: Bool = false - static let isDiscoverable: Bool = false + let dialog = IntentDialog(stringLiteral: UserText.vpnNeedsToBeEnabledFromApp) + throw needsToContinueInForegroundError(dialog) { + await UIApplication.shared.open(AppDeepLinkSchemes.openVPN.url) + } + default: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectFailure, error: error) - @MainActor - func perform() async throws -> some IntentResult { - do { - let managers = try await NETunnelProviderManager.loadAllFromPreferences() - guard let manager = managers.first, let session = manager.connection as? NETunnelProviderSession else { - return .result() + throw error } - - try? await session.sendProviderMessage(.cancelSnooze) - VPNReloadStatusWidgets() - await VPNSnoozeLiveActivityManager().endSnoozeActivity() - - return .result() - } catch { - return .result() - } - } - -} - -@available(iOS 17.0, *) -struct CancelSnoozeLiveActivityAppIntent: LiveActivityIntent { - - static var title: LocalizedStringResource = "Cancel Snooze" - static var isDiscoverable: Bool = false - static var openAppWhenRun: Bool = false - - func perform() async throws -> some IntentResult { - let managers = try await NETunnelProviderManager.loadAllFromPreferences() - guard let manager = managers.first, let session = manager.connection as? NETunnelProviderSession else { - return .result() } - - try? await session.sendProviderMessage(.cancelSnooze) - await VPNSnoozeLiveActivityManager().endSnoozeActivity() - VPNReloadStatusWidgets() - - return .result() } } 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/DuckDuckGo/VPNToggleIntent.swift b/DuckDuckGo/VPNToggleIntent.swift deleted file mode 100644 index c3b15f4a52..0000000000 --- a/DuckDuckGo/VPNToggleIntent.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// VPNIntents.swift -// DuckDuckGo -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import AppIntents -import NetworkExtension -import NetworkProtection -import WidgetKit -import Core - -// MARK: - Toggle - -@available(iOS 17.0, *) -struct VPNToggleIntent: SetValueIntent { - static let title: LocalizedStringResource = "Toggle DuckDuckGo VPN" - static let description: LocalizedStringResource = "Toggles the DuckDuckGo VPN" - - @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 - } - - //manager.connection.status - - manager.isOnDemandEnabled = false - try await manager.saveToPreferences() - manager.connection.stopVPNTunnel() - - 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 - } - - iterations += 1 - } - - VPNReloadStatusWidgets() - } catch { - // no-op - } - } -} 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/Widgets/ControlWidgetVPNIntents.swift b/Widgets/ControlWidgetVPNIntents.swift new file mode 100644 index 0000000000..11d9cfb366 --- /dev/null +++ b/Widgets/ControlWidgetVPNIntents.swift @@ -0,0 +1,100 @@ +// +// VPNIntents.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppIntents +import NetworkExtension +import NetworkProtection +import WidgetKit +import Core +import OSLog +import VPNWidgetSupport +import VPNAppIntents + +// 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 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 + + @MainActor + 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() + + 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() + + 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/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/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 00300d3d1a..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() : DisableVPNIntent() + 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: DisableVPNIntent()) + 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: EnableVPNIntent()) + 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/WidgetVPNIntents.swift b/Widgets/WidgetVPNIntents.swift new file mode 100644 index 0000000000..32609cd25c --- /dev/null +++ b/Widgets/WidgetVPNIntents.swift @@ -0,0 +1,159 @@ +// +// VPNIntents.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppIntents +import NetworkExtension +import NetworkProtection +import WidgetKit +import Core +import VPNWidgetSupport + +// 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 WidgetDisableVPNIntent: 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 = false + static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication + + @MainActor + func perform() async throws -> some IntentResult { + do { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectAttempt) + + let controller = VPNWidgetTunnelController() + try await controller.stop() + + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() + + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectSuccess) + return .result() + } catch VPNWidgetTunnelController.StopFailure.vpnNotConfigured { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectCancelled) + return .result() + } catch { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectFailure, error: error) + throw error + } + } +} + +/// 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 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 = false + static var authenticationPolicy: IntentAuthenticationPolicy = .alwaysAllowed + + @MainActor + func perform() async throws -> some IntentResult { + do { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectAttempt) + + let controller = VPNWidgetTunnelController() + try await controller.start() + + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() + + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectSuccess) + return .result() + } catch { + switch error { + case VPNWidgetTunnelController.StartFailure.vpnNotConfigured: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) + throw error + default: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectFailure, error: error) + throw error + } + } + } +} + +// MARK: - Snooze + +@available(iOS 17.0, *) +struct CancelSnoozeVPNIntent: AppIntent { + + static let title: LocalizedStringResource = "Resume VPN" + static let description: LocalizedStringResource = "Resumes the DuckDuckGo VPN" + static let openAppWhenRun: Bool = false + static let isDiscoverable: Bool = false + + @MainActor + func perform() async throws -> some IntentResult { + do { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + guard let manager = managers.first, let session = manager.connection as? NETunnelProviderSession else { + return .result() + } + + try? await session.sendProviderMessage(.cancelSnooze) + VPNReloadStatusWidgets() + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + + return .result() + } catch { + return .result() + } + } +} + +@available(iOS 17.0, *) +struct CancelSnoozeLiveActivityAppIntent: LiveActivityIntent { + + static var title: LocalizedStringResource = "Cancel Snooze" + static var isDiscoverable: Bool = false + static var openAppWhenRun: Bool = false + + func perform() async throws -> some IntentResult { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + guard let manager = managers.first, let session = manager.connection as? NETunnelProviderSession else { + return .result() + } + + try? await session.sendProviderMessage(.cancelSnooze) + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() + + return .result() + } +} 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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.";