diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 16091cf0c2..da15fb3ea8 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -483,8 +483,32 @@ 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 vpnShortcutConnectAttempt + case vpnShortcutConnectSuccess + case vpnShortcutConnectCancelled + case vpnShortcutConnectFailure + + case vpnShortcutDisconnectAttempt + case vpnShortcutDisconnectSuccess + case vpnShortcutDisconnectCancelled + case vpnShortcutDisconnectFailure case networkProtectionDNSUpdateCustom case networkProtectionDNSUpdateDefault @@ -1455,9 +1479,9 @@ extension Pixel.Event { case .remoteMessagePrimaryActionClicked: return "m_remote_message_primary_action_clicked" case .remoteMessageSecondaryActionClicked: return "m_remote_message_secondary_action_clicked" case .remoteMessageSheet: return "m_remote_message_sheet" - + // MARK: debug pixels - + case .dbCrashDetected: return "m_d_crash" case .dbCrashDetectedDaily: return "m_d_crash_daily" case .crashReportCRCIDMissing: return "m_crashreporting_crcid-missing" @@ -1761,8 +1785,32 @@ 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" + + case .vpnShortcutConnectAttempt: return "m_vpn_shortcut_connect_attempt" + case .vpnShortcutConnectSuccess: return "m_vpn_shortcut_connect_success" + case .vpnShortcutConnectCancelled: return "m_vpn_shortcut_connect_cancelled" + case .vpnShortcutConnectFailure: return "m_vpn_shortcut_connect_failure" + + case .vpnShortcutDisconnectAttempt: return "m_vpn_shortcut_disconnect_attempt" + case .vpnShortcutDisconnectSuccess: return "m_vpn_shortcut_disconnect_success" + case .vpnShortcutDisconnectCancelled: return "m_vpn_shortuct_disconnect_cancelled" + case .vpnShortcutDisconnectFailure: return "m_vpn_shortcut_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 91b6c160d0..793b76d777 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -203,7 +203,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 */; }; @@ -241,8 +240,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 */; }; @@ -379,10 +376,36 @@ 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 */; }; + 7B059F142D03881000371ED0 /* ControlCenterWidgetEducationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B059F132D03881000371ED0 /* ControlCenterWidgetEducationView.swift */; }; + 7B059F1D2D03A7E400371ED0 /* WidgetsShared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B059F1C2D03A7E400371ED0 /* WidgetsShared.xcassets */; }; + 7B059F1E2D03A7E400371ED0 /* WidgetsShared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B059F1C2D03A7E400371ED0 /* WidgetsShared.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 /* VPNShortcutIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1681042D10BC7B005EAE24 /* VPNShortcutIntents.swift */; }; + 7B1681092D10C678005EAE24 /* VPNControlStatusValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DC5BF2CB2A4A500EE5CC2 /* VPNControlStatusValueProvider.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 */; }; + 7B2CCBA32D11ABB100FE5852 /* VPNWidgetSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2CCBA22D11ABB100FE5852 /* VPNWidgetSupport */; }; + 7B2CCBA52D11ABBA00FE5852 /* VPNWidgetSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2CCBA42D11ABBA00FE5852 /* VPNWidgetSupport */; }; + 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 */; }; + 7B4F87EE2D0739EB0010B18F /* SiriBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4F87ED2D0739E80010B18F /* SiriBubbleView.swift */; }; 7B8E0EC62CC81B4900B2B722 /* TipKitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8E0EC52CC81B4800B2B722 /* TipKitController.swift */; }; 7BC571202BDBB877003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */; }; @@ -986,8 +1009,6 @@ CB825C962C071C9300BCC586 /* AlertViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB825C952C071C9300BCC586 /* AlertViewPresenter.swift */; }; CB84C7BD29A3EF530088A5B8 /* AppConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24F70E29A3EB15006DCC58 /* AppConfigurationURLProvider.swift */; }; CB941A6E2B96AB08000F9E7A /* PrivacyDashboard in Frameworks */ = {isa = PBXBuildFile; productRef = CB941A6D2B96AB08000F9E7A /* PrivacyDashboard */; }; - CB9B8739278C8E72001F4906 /* WidgetEducationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9B8738278C8E72001F4906 /* WidgetEducationViewController.swift */; }; - CB9B873C278C8FEA001F4906 /* WidgetEducationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9B873B278C8FEA001F4906 /* WidgetEducationView.swift */; }; CB9B873E278C93C2001F4906 /* HomeMessage.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CB9B873D278C93C2001F4906 /* HomeMessage.xcassets */; }; CBAA195A27BFE15600A4BD49 /* NSManagedObjectContextExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */; }; CBAD0EF92CFE1D3B006267B8 /* Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EF82CFE1D35006267B8 /* Init.swift */; }; @@ -1611,7 +1632,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 = ""; }; @@ -1737,14 +1757,32 @@ 6FF9AD3E2CE63DC200C5A406 /* TabSwitcherOpenDailyPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSwitcherOpenDailyPixel.swift; sourceTree = ""; }; 6FF9AD402CE6610600C5A406 /* TabSwitcherOpenDailyPixelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSwitcherOpenDailyPixelTests.swift; sourceTree = ""; }; 6FF9AD442CE766F700C5A406 /* NewTabPageControllerPixelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerPixelTests.swift; sourceTree = ""; }; + 7B059F0A2D0387E900371ED0 /* NumberedParagraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberedParagraphView.swift; sourceTree = ""; }; + 7B059F0C2D0387E900371ED0 /* WidgetEducationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetEducationView.swift; sourceTree = ""; }; + 7B059F0D2D0387E900371ED0 /* WidgetEducationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetEducationViewController.swift; sourceTree = ""; }; + 7B059F132D03881000371ED0 /* ControlCenterWidgetEducationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlCenterWidgetEducationView.swift; sourceTree = ""; }; + 7B059F1C2D03A7E400371ED0 /* WidgetsShared.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = WidgetsShared.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 /* VPNShortcutIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNShortcutIntents.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 = ""; }; + 7B4DC5BF2CB2A4A500EE5CC2 /* VPNControlStatusValueProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNControlStatusValueProvider.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 = ""; }; + 7B4F87ED2D0739E80010B18F /* SiriBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiriBubbleView.swift; sourceTree = ""; }; 7B8E0EC52CC81B4800B2B722 /* TipKitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitController.swift; sourceTree = ""; }; 7BC5711F2BDBB877003B0CCE /* VPNActivationDateStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNActivationDateStore.swift; sourceTree = ""; }; 7BDBAD0D2CBFB3F1000379B7 /* VPN.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = VPN.xcassets; sourceTree = ""; }; 7BF78E012CA2CC3E0026A1FC /* TipKitAppEventHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitAppEventHandling.swift; sourceTree = ""; }; + 7BFC32AF2CB291BB007A8E17 /* VPNControlWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNControlWidget.swift; sourceTree = ""; }; 7BFD5FD42C9DA310000FF959 /* VPNAddWidgetTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNAddWidgetTip.swift; sourceTree = ""; }; 7BFD5FD62C9DB9D7000FF959 /* VPNGeoswitchingTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNGeoswitchingTip.swift; sourceTree = ""; }; 7BFD5FD82C9DBC24000FF959 /* VPNSnoozeTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSnoozeTip.swift; sourceTree = ""; }; @@ -2885,8 +2923,6 @@ CB84C7C029A3F0280088A5B8 /* ConfigurationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationStore.swift; sourceTree = ""; }; CB8EF4A32AF6D4C200EF158D /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; CB8F1F7D2AF6D5370024BF0E /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; - CB9B8738278C8E72001F4906 /* WidgetEducationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetEducationViewController.swift; sourceTree = ""; }; - CB9B873B278C8FEA001F4906 /* WidgetEducationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetEducationView.swift; sourceTree = ""; }; CB9B873D278C93C2001F4906 /* HomeMessage.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = HomeMessage.xcassets; sourceTree = ""; }; CB9F2A4B2AF6D4FB00F924BB /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/InfoPlist.strings; sourceTree = ""; }; CBA1DE942AF6D579007C9457 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -3193,6 +3229,7 @@ 56D7792C2CFF476800B619EF /* StoreKit.framework in Frameworks */, 3760DFED299315EF0045A446 /* Waitlist in Frameworks */, F1D43AFA2B99C1D300BAB743 /* BareBonesBrowserKit in Frameworks */, + 7B2CCBA52D11ABBA00FE5852 /* VPNWidgetSupport in Frameworks */, F143C2EB1E4A4CD400CFDE3A /* Core.framework in Frameworks */, 31E69A63280F4CB600478327 /* DuckUI in Frameworks */, CB941A6E2B96AB08000F9E7A /* PrivacyDashboard in Frameworks */, @@ -3227,6 +3264,7 @@ 8512EA5124ED30D20073EE19 /* SwiftUI.framework in Frameworks */, 4BD96E062C4DBC93003BC32C /* NetworkExtension.framework in Frameworks */, 85DF714624F7FE6100C89288 /* Core.framework in Frameworks */, + 7B2CCBA32D11ABB100FE5852 /* VPNWidgetSupport in Frameworks */, 8512EA4F24ED30D20073EE19 /* WidgetKit.framework in Frameworks */, 4BBBBA872B02E85400D965DA /* DesignResourcesKit in Frameworks */, ); @@ -3871,9 +3909,10 @@ 31E69A60280F4BAD00478327 /* LocalPackages */ = { isa = PBXGroup; children = ( + 31794BFF2821DFB600F18633 /* DuckUI */, 85875B5F29912A2D00115F05 /* SyncUI */, + 7B10FF282D11AA0D00F36BF2 /* VPNiOS */, 37FCAACB2993149A000E420A /* Waitlist */, - 31794BFF2821DFB600F18633 /* DuckUI */, 315C77802CFA414400699683 /* AIChat */, ); path = LocalPackages; @@ -3937,7 +3976,8 @@ 4B5C46282AF2A6DB002A4432 /* Intents */ = { isa = PBXGroup; children = ( - 4B5C46292AF2A6E6002A4432 /* VPNIntents.swift */, + 7B1681042D10BC7B005EAE24 /* VPNShortcutIntents.swift */, + 7B4DC5E02CB2D87C00EE5CC2 /* VPNAutoShortcuts.swift */, ); name = Intents; sourceTree = ""; @@ -4207,6 +4247,44 @@ name = AdAttribution; sourceTree = ""; }; + 7B059F0B2D0387E900371ED0 /* NumberedParagraphView */ = { + isa = PBXGroup; + children = ( + 7B059F0A2D0387E900371ED0 /* NumberedParagraphView.swift */, + ); + path = NumberedParagraphView; + sourceTree = ""; + }; + 7B059F0E2D0387E900371ED0 /* Widgets */ = { + isa = PBXGroup; + children = ( + 7B4F87E32D0733B30010B18F /* Education */, + 7B4F87E62D0734060010B18F /* ControlCenterWidget.swift */, + ); + path = Widgets; + sourceTree = ""; + }; + 7B4F87E32D0733B30010B18F /* Education */ = { + isa = PBXGroup; + children = ( + 7B059F0B2D0387E900371ED0 /* NumberedParagraphView */, + 7B059F132D03881000371ED0 /* ControlCenterWidgetEducationView.swift */, + 7B059F0C2D0387E900371ED0 /* WidgetEducationView.swift */, + 7B059F0D2D0387E900371ED0 /* WidgetEducationViewController.swift */, + ); + path = Education; + sourceTree = ""; + }; + 7B4F87E82D0738D20010B18F /* Siri */ = { + isa = PBXGroup; + children = ( + 7B4F87ED2D0739E80010B18F /* SiriBubbleView.swift */, + 7B4F87E92D0738F40010B18F /* SiriEducationView.swift */, + 7B4F87EB2D07396A0010B18F /* SiriEducation.xcassets */, + ); + path = Siri; + sourceTree = ""; + }; 7BF78E002CA2CC100026A1FC /* TipKit */ = { isa = PBXGroup; children = ( @@ -4457,6 +4535,7 @@ C1B7B51D28941F160098FD6A /* RemoteMessaging */, F1AB2B401E3F75A000868554 /* Settings */, 0A6CC0EE23904D5400E4F627 /* Settings.bundle */, + 7B4F87E82D0738D20010B18F /* Siri */, D664C7922B289AA000CBFA76 /* Subscription */, 85F98F8C296F0ED100742F4A /* Sync */, F13B4BF41F18C74500814661 /* Tabs */, @@ -4469,6 +4548,7 @@ 84E341E31E2FC0E400BDBA6F /* UserInterfaceResources */, 3151F0E827357F8F00226F58 /* VoiceSearch */, 4B6484F427FD1E390050A7A1 /* Waitlist */, + 7B059F0E2D0387E900371ED0 /* Widgets */, ); path = DuckDuckGo; sourceTree = ""; @@ -4507,18 +4587,25 @@ isa = PBXGroup; children = ( 8512EA5624ED30D30073EE19 /* Assets.xcassets */, + 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 /* VPNControlStatusValueProvider.swift */, + 7B16810B2D10CF44005EAE24 /* WidgetVPNIntents.swift */, 4BB7CBAF2AF59C310014A35F /* VPNWidget.swift */, + 7BFC32AF2CB291BB007A8E17 /* VPNControlWidget.swift */, ); path = Widgets; sourceTree = ""; @@ -5536,15 +5623,6 @@ name = EmailAddressPrompt; sourceTree = ""; }; - CB1AEFB6279AF6420031AE3D /* WidgetEducation */ = { - isa = PBXGroup; - children = ( - CB9B8738278C8E72001F4906 /* WidgetEducationViewController.swift */, - CB9B873B278C8FEA001F4906 /* WidgetEducationView.swift */, - ); - name = WidgetEducation; - sourceTree = ""; - }; CB258D1129A4F1BB00DEBA24 /* Configuration */ = { isa = PBXGroup; children = ( @@ -6751,7 +6829,6 @@ C1B7B51B28941E980098FD6A /* HomeMessageViewModelBuilder.swift */, CBF14FC627970C8A001D94D0 /* HomeMessageCollectionViewCell.swift */, CB1AEFB02799AA940031AE3D /* SwiftUICollectionViewCell.swift */, - CB1AEFB6279AF6420031AE3D /* WidgetEducation */, ); name = HomeMessages; sourceTree = ""; @@ -6872,6 +6949,7 @@ 9F8FE9482BAE50E50071E372 /* Lottie */, 9F96F73A2C9144D5009E45D5 /* Onboarding */, 1E5918462CA422A7008ED2B3 /* Navigation */, + 7B2CCBA42D11ABBA00FE5852 /* VPNWidgetSupport */, 315C77812CFA41A400699683 /* AIChat */, ); productName = DuckDuckGo; @@ -6922,6 +7000,7 @@ name = WidgetsExtension; packageProductDependencies = ( 4BBBBA862B02E85400D965DA /* DesignResourcesKit */, + 7B2CCBA22D11ABB100FE5852 /* VPNWidgetSupport */, ); productName = WidgetsExtension; productReference = 8512EA4D24ED30D20073EE19 /* WidgetsExtension.appex */; @@ -7393,6 +7472,8 @@ 4B37E0502B928CA6009E81CA /* vpn-light-mode.json in Resources */, 9830A06325ED0DB900DB64DE /* BrowsingMenu.xcassets in Resources */, 98EF177D21837E35006750C1 /* new_tab_dark.json in Resources */, + 7B059F1E2D03A7E400371ED0 /* WidgetsShared.xcassets in Resources */, + 7B4F87EC2D07396A0010B18F /* SiriEducation.xcassets in Resources */, 85C2970A247EB7AA0063A335 /* Text.xcassets in Resources */, 98788E9A2521DA1E00D55218 /* Localizable.stringsdict in Resources */, CB9B873E278C93C2001F4906 /* HomeMessage.xcassets in Resources */, @@ -7419,6 +7500,7 @@ 98B001AA251EABB40090EC07 /* Localizable.strings in Resources */, 98B001A4251EABB40090EC07 /* InfoPlist.strings in Resources */, 4BCBE45E2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy in Resources */, + 7B059F1D2D03A7E400371ED0 /* WidgetsShared.xcassets in Resources */, 8512EA5724ED30D30073EE19 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -7655,6 +7737,7 @@ F1FDC9362BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, BDFF03212BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift in Sources */, EEFC6A602AC0F2F80065027D /* UserText.swift in Sources */, + 7B4DC5C42CB2B1D000EE5CC2 /* WidgetKind.swift in Sources */, 7BC571212BDBB977003B0CCE /* VPNActivationDateStore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -7714,6 +7797,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 */, @@ -7796,7 +7880,6 @@ 7BFD5FD92C9DBC24000FF959 /* VPNSnoozeTip.swift in Sources */, 98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */, 4B2C79612C5B27AC00A240CC /* VPNSnoozeActivityAttributes.swift in Sources */, - CB9B873C278C8FEA001F4906 /* WidgetEducationView.swift in Sources */, 85F200002215C17B006BB258 /* FindInPage.swift in Sources */, F1386BA41E6846C40062FC3C /* TabDelegate.swift in Sources */, 37CF91602BB4737300BADCAE /* CrashCollectionOnboarding.swift in Sources */, @@ -7836,13 +7919,14 @@ 1E908BF329827C480008C8F3 /* AutoconsentManagement.swift in Sources */, D6D95CE32B6D9F8800960317 /* AsyncHeadlessWebViewModel.swift in Sources */, C185ED612BD4329700BAE9DC /* ImportPasswordsStatusHandler.swift in Sources */, - CB9B8739278C8E72001F4906 /* WidgetEducationViewController.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 */, 9F8E0F262CC9395D001EA7C5 /* Logger+Onboarding.swift in Sources */, AA3D854523D9942200788410 /* AppIconSettingsViewController.swift in Sources */, + 7B059F142D03881000371ED0 /* ControlCenterWidgetEducationView.swift in Sources */, 85C297042476C1FD0063A335 /* DaxDialogsSettings.swift in Sources */, 8505836F219F424500ED4EDB /* UIViewExtension.swift in Sources */, D6F93E3C2B4FFA97004C268D /* SubscriptionDebugViewController.swift in Sources */, @@ -7875,6 +7959,9 @@ 37FCAABC2992F592000E420A /* MultilineScrollableTextFix.swift in Sources */, 85DFEDED24C7CCA500973FE7 /* AppWidthObserver.swift in Sources */, 4B6484F327FD1E350050A7A1 /* MenuControllerView.swift in Sources */, + 7B059F0F2D0387E900371ED0 /* NumberedParagraphView.swift in Sources */, + 7B059F112D0387E900371ED0 /* WidgetEducationViewController.swift in Sources */, + 7B059F122D0387E900371ED0 /* WidgetEducationView.swift in Sources */, 9F9A92342C86B42B001D036D /* AppIconPicker.swift in Sources */, CB825C962C071C9300BCC586 /* AlertViewPresenter.swift in Sources */, 1EE7C299294227EC0026C8CB /* AutoconsentSettingsViewController.swift in Sources */, @@ -7943,6 +8030,7 @@ 1EEF12502851016B003DDE57 /* PrivacyIconAndTrackersAnimator.swift in Sources */, 31CB4251273AF50700FA0F3F /* SpeechRecognizerProtocol.swift in Sources */, 319A37172829C8AD0079FBCE /* UITableViewExtension.swift in Sources */, + 7B1681062D10BC96005EAE24 /* VPNShortcutIntents.swift in Sources */, 85EE7F59224673C5000FE757 /* WebContainerNavigationController.swift in Sources */, 6FD1BAE52B87A107000C475C /* AdAttributionReporterStorage.swift in Sources */, 6F5CC0812C2AFFE400AFC840 /* ToggleExpandButtonStyle.swift in Sources */, @@ -7988,11 +8076,13 @@ 9FDEC7C12C9127F100C7A692 /* OnboardingAddressBarPositionPickerViewModel.swift in Sources */, 6F9FFE282C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift in Sources */, 85374D3C21AC41E700FF5A1E /* FavoritesHomeViewSectionRenderer.swift in Sources */, + 7B4F87EE2D0739EB0010B18F /* SiriBubbleView.swift in Sources */, 85DFEDF124C7EEA400973FE7 /* LargeOmniBarState.swift in Sources */, 9880722A25FA497B0039EF4B /* MenuButton.swift in Sources */, D6E83C602B22B3C9006C8AFB /* SettingsState.swift in Sources */, D6E83C482B20C812006C8AFB /* SettingsHostingController.swift in Sources */, F46FEC5727987A5F0061D9DF /* KeychainItemsDebugViewController.swift in Sources */, + 7B4F87E72D0734090010B18F /* ControlCenterWidget.swift in Sources */, 9F7CFF762C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift in Sources */, D68A21442B7EC08500BB372E /* SubscriptionExternalLinkView.swift in Sources */, BDD3B3552B8EF8DB005857A8 /* NetworkProtectionUNNotificationPresenter.swift in Sources */, @@ -8000,7 +8090,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 */, @@ -8022,6 +8111,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 */, @@ -8080,6 +8170,7 @@ CB825C922C071B1400BCC586 /* AlertView.swift in Sources */, 1DDF40292BA04FCD006850D9 /* SettingsPrivacyProtectionsView.swift in Sources */, BDE91CD82C629A910005CB74 /* UnifiedFeedbackSender.swift in Sources */, + 7B4DC5C32CB2AF0700EE5CC2 /* WidgetKind.swift in Sources */, 6F64AA5F2C49463C00CF4489 /* ShortcutsModel.swift in Sources */, F1D477C61F2126CC0031ED49 /* OmniBarState.swift in Sources */, 85F2FFCD2211F615006BB258 /* MainViewController+KeyCommands.swift in Sources */, @@ -8122,6 +8213,7 @@ F1CA3C391F045885005FADB3 /* PrivacyUserDefaults.swift in Sources */, 6F655BE22BAB289E00AC3597 /* DefaultTheme.swift in Sources */, 6FE1274B2C20943500EB5724 /* ShortcutItemView.swift in Sources */, + 7B4F87EA2D0738F90010B18F /* SiriEducationView.swift in Sources */, BD862E072B30F5E30073E2EE /* VPNFeedbackSender.swift in Sources */, AA4D6A6A23DB87B1007E8790 /* AppIconManager.swift in Sources */, 8563A03C1F9288D600F04442 /* BrowserChromeManager.swift in Sources */, @@ -8152,6 +8244,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 */, @@ -8168,7 +8261,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 */, @@ -8241,6 +8333,7 @@ 46DD3D5A2D0A29F600F33D49 /* CrashReportSenderExtensions.swift in Sources */, 1DEAADEA2BA4539800E25A97 /* SettingsAppearanceView.swift in Sources */, B623C1C22862CA9E0043013E /* DownloadSession.swift in Sources */, + 7B16810C2D10CF44005EAE24 /* WidgetVPNIntents.swift in Sources */, 317CA3432CFF82E100F88848 /* SettingsAIChatView.swift in Sources */, 9F7CFF7F2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift in Sources */, 985892522260B1B200EEB31B /* ProgressView.swift in Sources */, @@ -8514,12 +8607,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 */, + 7B16810A2D10C680005EAE24 /* VPNControlWidget.swift in Sources */, 853273B324FF114700E3C778 /* DeepLinks.swift in Sources */, + 7B4DC5C22CB2AE4600EE5CC2 /* WidgetKind.swift in Sources */, 853273B424FFB36100E3C778 /* UIColorExtension.swift in Sources */, 853273AB24FEF27500E3C778 /* WidgetViews.swift in Sources */, - 4B5C462B2AF2BDC4002A4432 /* VPNIntents.swift in Sources */, + 7B1681092D10C678005EAE24 /* VPNControlStatusValueProvider.swift in Sources */, + 7B16810D2D10CF44005EAE24 /* WidgetVPNIntents.swift in Sources */, 4BB7CBB02AF59C310014A35F /* VPNWidget.swift in Sources */, 8512EA5424ED30D20073EE19 /* Widgets.swift in Sources */, 85DB12EB2A1FE2A4000A4A72 /* LockScreenWidgets.swift in Sources */, @@ -11763,7 +11861,7 @@ repositoryURL = "https://github.com/duckduckgo/DesignResourcesKit"; requirement = { kind = exactVersion; - version = 3.3.0; + version = 3.3.1; }; }; F486D2EF25069482002D07D7 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { @@ -11903,6 +12001,14 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = PixelKit; }; + 7B2CCBA22D11ABB100FE5852 /* VPNWidgetSupport */ = { + isa = XCSwiftPackageProductDependency; + productName = VPNWidgetSupport; + }; + 7B2CCBA42D11ABBA00FE5852 /* VPNWidgetSupport */ = { + isa = XCSwiftPackageProductDependency; + productName = VPNWidgetSupport; + }; 851481872A600EFC00ABC65F /* RemoteMessaging */ = { isa = XCSwiftPackageProductDependency; package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5f7e2bbb19..e611969fa1 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/DesignResourcesKit", "state" : { - "revision" : "ad133f76501edcb2bfa841e33aebc0da5f92bb5c", - "version" : "3.3.0" + "revision" : "a35414a0b07da7fb79255370edfe845b7a22558c", + "version" : "3.3.1" } }, { diff --git a/DuckDuckGo/DuckDuckGo.entitlements b/DuckDuckGo/DuckDuckGo.entitlements index ee77dff2bf..b8a6abb15d 100644 --- a/DuckDuckGo/DuckDuckGo.entitlements +++ b/DuckDuckGo/DuckDuckGo.entitlements @@ -10,6 +10,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 60338d9de6..c3233c5e87 100644 --- a/DuckDuckGo/DuckDuckGoAlpha.entitlements +++ b/DuckDuckGo/DuckDuckGoAlpha.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/HomeMessage.xcassets/ControlCenterWidget/Contents.json b/DuckDuckGo/HomeMessage.xcassets/ControlCenterWidget/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/DuckDuckGo/HomeMessage.xcassets/ControlCenterWidget/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/HomeMessage.xcassets/ControlCenterWidget/ControlCenterBottom.imageset/Contents.json b/DuckDuckGo/HomeMessage.xcassets/ControlCenterWidget/ControlCenterBottom.imageset/Contents.json new file mode 100644 index 0000000000..ee52a1ced9 --- /dev/null +++ b/DuckDuckGo/HomeMessage.xcassets/ControlCenterWidget/ControlCenterBottom.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Frame 624697 1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/HomeMessage.xcassets/ControlCenterWidget/ControlCenterBottom.imageset/Frame 624697 1.pdf b/DuckDuckGo/HomeMessage.xcassets/ControlCenterWidget/ControlCenterBottom.imageset/Frame 624697 1.pdf new file mode 100644 index 0000000000..c2c7b177ff Binary files /dev/null and b/DuckDuckGo/HomeMessage.xcassets/ControlCenterWidget/ControlCenterBottom.imageset/Frame 624697 1.pdf differ diff --git a/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/Contents.json b/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/Contents.json index 066a908387..cb7ad9de3c 100644 --- a/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/Contents.json +++ b/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "instructions@2x.png", + "filename" : "Frame 624697.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/Frame 624697.pdf b/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/Frame 624697.pdf new file mode 100644 index 0000000000..502b60e456 Binary files /dev/null and b/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/Frame 624697.pdf differ diff --git a/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreeniOS17.imageset/Contents.json b/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreeniOS17.imageset/Contents.json new file mode 100644 index 0000000000..066a908387 --- /dev/null +++ b/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreeniOS17.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "instructions@2x.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/instructions@2x.png b/DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreeniOS17.imageset/instructions@2x.png similarity index 100% rename from DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreen.imageset/instructions@2x.png rename to DuckDuckGo/HomeMessage.xcassets/WidgetEducation/WidgetEducationHomeScreeniOS17.imageset/instructions@2x.png diff --git a/DuckDuckGo/NetworkProtectionStatusView.swift b/DuckDuckGo/NetworkProtectionStatusView.swift index fa8ae1ff0a..24dd47e31c 100644 --- a/DuckDuckGo/NetworkProtectionStatusView.swift +++ b/DuckDuckGo/NetworkProtectionStatusView.swift @@ -65,7 +65,9 @@ struct NetworkProtectionStatusView: View { }) .applyInsetGroupedListStyle() .sheet(isPresented: $statusModel.showAddWidgetEducationView) { - widgetEducationSheet() + if #available(iOS 17.0, *) { + widgetEducationSheet() + } } .onAppear { if #available(iOS 18.0, *) { @@ -413,6 +415,7 @@ struct NetworkProtectionStatusView: View { // MARK: - Sheets + @available(iOS 17.0, *) private func widgetEducationSheet() -> some View { NavigationView { WidgetEducationView() diff --git a/DuckDuckGo/NetworkProtectionStatusViewModel.swift b/DuckDuckGo/NetworkProtectionStatusViewModel.swift index 74732af3fe..3d6ef4f9fa 100644 --- a/DuckDuckGo/NetworkProtectionStatusViewModel.swift +++ b/DuckDuckGo/NetworkProtectionStatusViewModel.swift @@ -468,7 +468,7 @@ final class NetworkProtectionStatusViewModel: ObservableObject { await disableNetP() } - WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") + VPNReloadStatusWidgets() } @MainActor diff --git a/DuckDuckGo/NetworkProtectionVPNSettingsView.swift b/DuckDuckGo/NetworkProtectionVPNSettingsView.swift index b01354f454..9b431d22a1 100644 --- a/DuckDuckGo/NetworkProtectionVPNSettingsView.swift +++ b/DuckDuckGo/NetworkProtectionVPNSettingsView.swift @@ -26,24 +26,14 @@ struct NetworkProtectionVPNSettingsView: View { var body: some View { VStack { List { - // Widget only available for iOS 17 and up - if #available(iOS 17.0, *) { - Section { - NavigationLink { - WidgetEducationView.vpn - } label: { - Text(UserText.vpnSettingsAddWidget).daxBodyRegular() - } - } - .listRowBackground(Color(designSystemColor: .surface)) - } - switch viewModel.viewKind { case .loading: EmptyView() case .unauthorized: notificationsUnauthorizedView case .authorized: notificationAuthorizedView } + shortcutsView + toggleSection( text: UserText.netPExcludeLocalNetworksSettingTitle, headerText: UserText.netPExcludeLocalNetworksSettingHeader, @@ -156,17 +146,68 @@ struct NetworkProtectionVPNSettingsView: View { .listRowBackground(Color(designSystemColor: .surface)) } + @ViewBuilder + private var shortcutsView: some View { + // Widget only available for iOS 17 and up + if #available(iOS 17.0, *) { + Section { + NavigationLink { + WidgetEducationView.vpn + } label: { + Label { + Text(UserText.vpnSettingsAddWidget) + } icon: { + Image(.addWidgetColor24) + .frame(width: 24, height: 24) + }.daxBodyRegular() + } + + #if ALPHA || DEBUG + if #available(iOS 18.0, *) { + NavigationLink { + ControlCenterWidgetEducationView(navBarTitle: "Add DuckDuckGo VPN Shortcut to Your Control Center", + widget: .vpnToggle) + } label: { + Label { + Text(UserText.vpnSettingsAddControlCenterWidget) + } icon: { + Image(.settingsColor24) + .frame(width: 24, height: 24) + }.daxBodyRegular() + } + } + + NavigationLink { + SiriEducationView() + } label: { + Label { + Text(UserText.vpnSettingsControlWithSiri) + } icon: { + Image(.askSiriColor24) + .frame(width: 24, height: 24) + }.daxBodyRegular() + } + #endif + } header: { + Text(UserText.netPVPNShortcutsSectionHeader) + } + .listRowBackground(Color(designSystemColor: .surface)) + } + } } +@available(iOS 17.0, *) private extension WidgetEducationView { + static var vpn: Self { WidgetEducationView( - navBarTitle: UserText.vpnSettingsAddWidget, + navBarTitle: UserText.settingsAddVPNWidget, thirdParagraphText: UserText.addVPNWidgetSettingsThirdParagraph, - widgetExampleImageConfig: .init( - image: Image("WidgetEducationVPNWidgetExample"), + thirdParagraphDetail: .image( + Image("WidgetEducationVPNWidgetExample"), maxWidth: 164, - horizontalOffset: -7 + horizontalOffset: -7, + dropsShadow: true ) ) } diff --git a/DuckDuckGo/NetworkProtectionWidgetRefreshModel.swift b/DuckDuckGo/NetworkProtectionWidgetRefreshModel.swift index 0e8fcfe2b3..1abb6fabc2 100644 --- a/DuckDuckGo/NetworkProtectionWidgetRefreshModel.swift +++ b/DuckDuckGo/NetworkProtectionWidgetRefreshModel.swift @@ -36,7 +36,7 @@ class NetworkProtectionWidgetRefreshModel { } public func refreshVPNWidget() { - WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") + VPNReloadStatusWidgets() } } diff --git a/DuckDuckGo/Siri/SiriBubbleView.swift b/DuckDuckGo/Siri/SiriBubbleView.swift new file mode 100644 index 0000000000..5de3b184dd --- /dev/null +++ b/DuckDuckGo/Siri/SiriBubbleView.swift @@ -0,0 +1,76 @@ +// +// SiriBubbleView.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 DesignResourcesKit +import SwiftUICore + +private struct SiriBubble: Shape { + + static let tipHeight: CGFloat = 7 + + func path(in rect: CGRect) -> Path { + var path = Path() + let cornerRadius: CGFloat = 21 + let tipHeight: CGFloat = 7 + let tipWidth: CGFloat = 9 + + // Rounded rectangle portion + let roundedRect = CGRect( + x: rect.minX, + y: rect.minY, + width: rect.width, + height: rect.height - Self.tipHeight + ) + path.addRoundedRect(in: roundedRect, cornerSize: CGSize(width: cornerRadius, height: cornerRadius)) + + // Triangle tip drawn out of bounds + let tipStartX = rect.maxX - (cornerRadius + 2 * tipWidth) + let tipBaseY = rect.maxY - Self.tipHeight + + path.move(to: CGPoint(x: tipStartX, y: tipBaseY)) // Bottom-right corner of rounded rectangle + path.addLine(to: CGPoint(x: tipStartX + tipWidth, y: tipBaseY)) // Tip top-right + path.addLine(to: CGPoint(x: tipStartX, y: tipBaseY + tipHeight)) // Tip bottom-right (out of bounds) + path.closeSubpath() + + return path + } +} + +struct SiriBubbleView: View { + + let text: String + + init(_ text: String) { + self.text = text + } + + var body: some View { + HStack(alignment: .center) { + Text(text) + .foregroundStyle(Color(designSystemColor: .textPrimary)) + .multilineTextAlignment(.center) + }.padding(12) + .padding(.bottom, SiriBubble.tipHeight) + .frame(maxWidth: .infinity) + .background(SiriBubble() + .fill(Color(designSystemColor: .surface)) + .shadow(color: .black.opacity(0.08), radius: 8, x: 0, y: 8) + .shadow(color: .black.opacity(0.1), radius: 3, x: 0, y: 2)) + } +} diff --git a/DuckDuckGo/Siri/SiriEducation.xcassets/Contents.json b/DuckDuckGo/Siri/SiriEducation.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/DuckDuckGo/Siri/SiriEducation.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Siri/SiriEducation.xcassets/Images/Contents.json b/DuckDuckGo/Siri/SiriEducation.xcassets/Images/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/DuckDuckGo/Siri/SiriEducation.xcassets/Images/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Siri/SiriEducation.xcassets/Images/Siri-Control-128.imageset/Contents.json b/DuckDuckGo/Siri/SiriEducation.xcassets/Images/Siri-Control-128.imageset/Contents.json new file mode 100644 index 0000000000..02cd157c23 --- /dev/null +++ b/DuckDuckGo/Siri/SiriEducation.xcassets/Images/Siri-Control-128.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Siri-Control-128.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Siri/SiriEducation.xcassets/Images/Siri-Control-128.imageset/Siri-Control-128.pdf b/DuckDuckGo/Siri/SiriEducation.xcassets/Images/Siri-Control-128.imageset/Siri-Control-128.pdf new file mode 100644 index 0000000000..2049ae7de4 Binary files /dev/null and b/DuckDuckGo/Siri/SiriEducation.xcassets/Images/Siri-Control-128.imageset/Siri-Control-128.pdf differ diff --git a/DuckDuckGo/Siri/SiriEducationView.swift b/DuckDuckGo/Siri/SiriEducationView.swift new file mode 100644 index 0000000000..a844b7fded --- /dev/null +++ b/DuckDuckGo/Siri/SiriEducationView.swift @@ -0,0 +1,100 @@ +// +// SiriEducationView.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 SwiftUI +import DesignResourcesKit +import Core + +struct SiriEducationView: View { + typealias Detail = NumberedParagraphConfig.Detail + + enum Padding { + static let top: CGFloat = 24 + } + + enum Spacing { + static let aboveHeader: CGFloat = 8 + static let headerToList: CGFloat = 32 + static let headerInterContent: CGFloat = 13 + static let sidesToContent: CGFloat = 24 + } + + enum Size { + static let exampleImageWidth: CGFloat = 270 + } + + @Environment(\.dismiss) private var dismiss + + let thirdParagraphText: String + let thirdParagraphDetail: Detail + + init(thirdParagraphText: String = UserText.addWidgetSettingsThirdParagraph, + thirdParagraphDetail: Detail = .image(Image.widgetExample, maxWidth: Size.exampleImageWidth)) { + + self.thirdParagraphText = thirdParagraphText + self.thirdParagraphDetail = thirdParagraphDetail + } + + var body: some View { + ScrollView { + VStack(alignment: .center, spacing: Spacing.headerInterContent) { + Image(.siriControl128) + .resizable() + .frame(maxWidth: 128) + + Text(UserText.vpnControlWidgetEducationScreenTitle) + .font(.system(size: 22, weight: .bold, design: .default)) + .kerning(0.35) + .multilineTextAlignment(.center) + + Text(UserText.vpnControlWidgetEducationScreenDescription) + .font(.system(size: 16, weight: .regular)) + .multilineTextAlignment(.center) + } + .padding(.top, Spacing.aboveHeader) + .padding(.horizontal, Spacing.sidesToContent) + + VStack(alignment: .leading, spacing: 16) { + SiriBubbleView(UserText.vpnControlWidgetEducationScreenExample1) + + SiriBubbleView(UserText.vpnControlWidgetEducationScreenExample2) + + SiriBubbleView(UserText.vpnControlWidgetEducationScreenExample3) + } + .padding(.top, Spacing.headerToList) + .padding(.horizontal, Spacing.sidesToContent) + } + .navigationBarTitle("") + .background(Color.background) + } +} + +private extension Color { + static let background = Color(designSystemColor: .background) + static let font = Color(designSystemColor: .textPrimary) +} + +/* +@available(iOS 17.0, *) +struct WidgetEducationView_Previews: PreviewProvider { + static var previews: some View { + WidgetEducationView() + } +} +*/ diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index c6e7a5722f..884595d196 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -341,10 +341,11 @@ public struct UserText { public static let addWidget = NSLocalizedString("addWidget.button", value: "Add Widget", comment: "") public static let addWidgetTitle = NSLocalizedString("addWidget.title", value: "One tap to your favorite sites.", comment: "") public static let addWidgetDescription = NSLocalizedString("addWidget.description", value: "Get quick access to private search and the sites you love.", comment: "") - public static let addWidgetSettingsFirstParagraph = NSLocalizedString("addWidget.settings.firstParagraph", value: "Long-press on the Home Screen to enter jiggle mode.", comment: "") - public static let addWidgetSettingsSecondParagraph = NSLocalizedString("addWidget.settings.secondParagraph.%@", value: "Tap the plus %@ button.", comment: "Replacement string is a plus button icon.") - public static let addWidgetSettingsThirdParagraph = NSLocalizedString("addWidget.settings.title", value: "Find and select DuckDuckGo. Then choose a widget.", comment: "") - + public static let addWidgetSettingsFirstParagraph = NSLocalizedString("addWidget.settings.firstParagraph", value: "Long-press anywhere on the Home Screen until the apps jiggle", comment: "") + public static let addWidgetSettingsSecondParagraphiOS17 = NSLocalizedString("addWidget.settings.secondParagraph.%@", value: "Tap the plus %@ button.", comment: "Replacement string is a plus button icon.") + public static let addWidgetSettingsSecondParagraph = NSLocalizedString("addWidget.settings.secondParagraph", value: "Tap the **Edit** button, then choose **Add Widget**", comment: "") + public static let addWidgetSettingsThirdParagraph = NSLocalizedString("addWidget.settings.title", value: "Find and select **DuckDuckGo**. Then choose a widget.", comment: "") + public static let actionSaveToDownloads = NSLocalizedString("downloads.alert.action.save-to-downloads", value: "Save to Downloads", comment: "Alert action for starting a file dowload") public static func messageDownloadStarted(for filename: String) -> String { let message = NSLocalizedString("downloads.message.download-started", value: "Download started for %@", comment: "Message confirming that the download process has started. Parameter is downloaded file's filename") @@ -451,6 +452,16 @@ public struct UserText { public static let actionAutofillLogins = NSLocalizedString("action.title.autofill.logins", value: "Passwords", comment: "Autofill Logins menu item opening the login list") + // MARK: - Control Center Widget Education + + public static let controlCenterWidgetEducationParagraph1 = NSLocalizedString("control.center.widget.education.paragraph.1", value: "Swipe down from the top-right corner of the screen", comment: "First paragraph of the Control Center Widget Education screen") + public static let controlCenterWidgetEducationParagraph2 = NSLocalizedString("control.center.widget.education.paragraph.2", value: "Tap the **+** button at the top left of **Control Center**", comment: "Second paragraph of the Control Center Widget Education screen") + public static let controlCenterWidgetEducationParagraph3 = NSLocalizedString("control.center.widget.education.paragraph.3", value: "Tap **Add a Control** at the bottom of the screen", comment: "Third paragraph of the Control Center Widget Education screen") + + // MARK: - Control Center Widget Education: VPN + + public static let controlCenterVPNWidgetEducationParagraph = NSLocalizedString("control.center.vpn.widget.education.paragraph", value: "Find **DuckDuckGo** in the list, then tap **VPN control** to add it", comment: "Fourth paragraph of the Control Center VPN Widget Education screen") + // MARK: - Waitlist public static let waitlistPrivacyDisclaimer = NSLocalizedString("waitlist.privacy-disclaimer", @@ -571,6 +582,7 @@ public struct UserText { static let netPVPNAlertsSectionHeader = NSLocalizedString("network.protection.vpn.alerts.section.header", value: "Notifications", comment: "Section header for the toggle for VPN notifications.") static let netPVPNAlertsToggleTitle = NSLocalizedString("network.protection.vpn.alerts.toggle.title", value: "VPN Notifications", comment: "Title for the toggle for VPN notifications.") static let netPVPNAlertsToggleSectionFooter = NSLocalizedString("network.protection.vpn.alerts.toggle.section.footer", value: "Get notified if your connection drops or VPN status changes.", comment: "List section footer for the toggle for VPN alerts.") + static let netPVPNShortcutsSectionHeader = NSLocalizedString("network.protection.vpn.shortcuts.section.header", value: "Shortcuts", comment: "Section header for the shortcuts section in VPN settings.") static let netPFrequentlyAskedQuestionsTitle = NSLocalizedString("network.protection.faq.title", value: "DuckDuckGo VPN FAQ", comment: "Title for the VPN FAQ screen.") static let netPOpenVPNQuickAction = NSLocalizedString("network.protection.quick-action.open-vpn", value: "Open VPN", comment: "Title text for an iOS quick action that opens VPN settings") @@ -677,10 +689,28 @@ public struct UserText { static let itrFeedbackFormCategoryUnhelpful = NSLocalizedString("feedback.itr.category.unhelpful", value: "Call to Advisor was unhelpful", comment: "Category for unhelpful advisor calls") static let itrFeedbackFormCategorySomethingElse = NSLocalizedString("feedback.itr.category.something-else", value: "Something else", comment: "Category for other Identity Theft Restoration issues") - // MARK: VPN Widget + // MARK: VPN Shortcuts public static let vpnSettingsAddWidget = NSLocalizedString("vpn.settings.add.widget", value: "Add VPN Widget to Home Screen", comment: "VPN settings screen cell text for adding the VPN widget to the home screen") - public static let addVPNWidgetSettingsThirdParagraph = NSLocalizedString("vpn.addWidget.settings.title", value: "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget.", comment: "Title for the VPN widget onboarding screen") + public static let vpnSettingsAddControlCenterWidget = NSLocalizedString("vpn.settings.add.control-center.widget", value: "Add VPN to Control Center", comment: "VPN settings screen cell text for adding the VPN widget to the control center") + public static let vpnSettingsControlWithSiri = NSLocalizedString("vpn.settings.control.with.siri", value: "Control VPN With Siri", comment: "VPN settings screen cell text to learn how to control the VPN with Siri") + + // MARK: - VPN Widget + + public static let addVPNWidgetSettingsThirdParagraph = NSLocalizedString("vpn.addWidget.settings.title", value: "Find and select **DuckDuckGo**, then swipe to **VPN** and select **Add Widget**", comment: "Title for the VPN widget onboarding screen") + + // MARK: - VPN Control Widget + + public static let vpnControlWidgetEnableVPNIntentSuccess = NSLocalizedString("vpn.control-widget.enable.vpn.intent.success", value: "DuckDuckGo VPN is connected", comment: "Message for success when running the intent to enable the DuckDuckGo VPN") + public static let vpnControlWidgetDisableVPNIntentSuccess = NSLocalizedString("vpn.control-widget.disable.vpn.intent.success", value: "DuckDuckGo VPN is connected", comment: "Message for success when running the intent to enable the DuckDuckGo VPN") + + // MARK: - VPN Control Widget Education View + + public static let vpnControlWidgetEducationScreenTitle = NSLocalizedString("vpn.control-widget.education.screen.title", value: "Control DuckDuckGo VPN with Siri!", comment: "Title for the VPN Control Widget education screen title") + public static let vpnControlWidgetEducationScreenDescription = NSLocalizedString("vpn.control-widget.education.screen.description", value: "Siri Shortcuts let you connect to your VPN with a quick voice command, like “Siri, turn on DuckDuckGo VPN”. It’s a fast, hands-free way to use your VPN.", comment: "") + public static let vpnControlWidgetEducationScreenExample1 = NSLocalizedString("vpn.control-widget.education.screen.example1", value: "Siri, start DuckDuckGo VPN.", comment: "Siri commands education screen: example 1") + public static let vpnControlWidgetEducationScreenExample2 = NSLocalizedString("vpn.control-widget.education.screen.example2", value: "Siri, enable DuckDuckGo VPN.", comment: "Siri commands education screen: example 2") + public static let vpnControlWidgetEducationScreenExample3 = NSLocalizedString("vpn.control-widget.education.screen.example3", value: "Siri, protect my device with DuckDuckGo.", comment: "Siri commands education screen: example 3") // MARK: Custom DNS @@ -1067,6 +1097,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let nextSteps = NSLocalizedString("settings.next.steps", value: "Next Steps", comment: "The name of a settings category listing next steps") public static let settingsAddToDock = NSLocalizedString("settings.add.to.dock", value: "Add App to Your Dock", comment: "Settings screen cell text for adding the app to the dock") public static let settingsAddWidget = NSLocalizedString("settings.add.widget", value: "Add Widget to Home Screen", comment: "Settings screen cell text for add widget to the home screen") + public static let settingsAddVPNWidget = NSLocalizedString("settings.add.widget.vpn", value: "Add DuckDuckGo VPN Widget to Your Home Screen", comment: "Title for the settings VPN subsection where the user can learn how to add the VPN to their home screen") public static let setYourAddressBarPosition = NSLocalizedString("settings.set.your.address.bar.position", value: "Set Your Address Bar Position", comment: "Settings screen cell text for setting address bar position") public static let enableVoiceSearch = NSLocalizedString("settings.enable.voice.search", value: "Enable Voice Search", comment: "Settings screen cell text for enabling voice search") diff --git a/DuckDuckGo/VPN.xcassets/Add-Widget-Color-24.imageset/Add-Widget-Color-24.pdf b/DuckDuckGo/VPN.xcassets/Add-Widget-Color-24.imageset/Add-Widget-Color-24.pdf new file mode 100644 index 0000000000..5384936539 Binary files /dev/null and b/DuckDuckGo/VPN.xcassets/Add-Widget-Color-24.imageset/Add-Widget-Color-24.pdf differ diff --git a/DuckDuckGo/VPN.xcassets/Add-Widget-Color-24.imageset/Contents.json b/DuckDuckGo/VPN.xcassets/Add-Widget-Color-24.imageset/Contents.json new file mode 100644 index 0000000000..b04419ba8f --- /dev/null +++ b/DuckDuckGo/VPN.xcassets/Add-Widget-Color-24.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Add-Widget-Color-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/VPN.xcassets/AskSiri-Color-24.imageset/AskSiri-Color-24-2.pdf b/DuckDuckGo/VPN.xcassets/AskSiri-Color-24.imageset/AskSiri-Color-24-2.pdf new file mode 100644 index 0000000000..0b51543e75 Binary files /dev/null and b/DuckDuckGo/VPN.xcassets/AskSiri-Color-24.imageset/AskSiri-Color-24-2.pdf differ diff --git a/DuckDuckGo/VPN.xcassets/AskSiri-Color-24.imageset/Contents.json b/DuckDuckGo/VPN.xcassets/AskSiri-Color-24.imageset/Contents.json new file mode 100644 index 0000000000..09d8b3fe3b --- /dev/null +++ b/DuckDuckGo/VPN.xcassets/AskSiri-Color-24.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AskSiri-Color-24-2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/VPN.xcassets/Settings-Color-24.imageset/Contents.json b/DuckDuckGo/VPN.xcassets/Settings-Color-24.imageset/Contents.json new file mode 100644 index 0000000000..f4d6014768 --- /dev/null +++ b/DuckDuckGo/VPN.xcassets/Settings-Color-24.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Settings-Color-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/VPN.xcassets/Settings-Color-24.imageset/Settings-Color-24.pdf b/DuckDuckGo/VPN.xcassets/Settings-Color-24.imageset/Settings-Color-24.pdf new file mode 100644 index 0000000000..6f4ffecc73 Binary files /dev/null and b/DuckDuckGo/VPN.xcassets/Settings-Color-24.imageset/Settings-Color-24.pdf differ diff --git a/DuckDuckGo/VPNAutoShortcuts.swift b/DuckDuckGo/VPNAutoShortcuts.swift new file mode 100644 index 0000000000..4944fcb418 --- /dev/null +++ b/DuckDuckGo/VPNAutoShortcuts.swift @@ -0,0 +1,63 @@ +// +// 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 + +#if ALPHA || DEBUG +@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 the VPN connection with \(.applicationName)" + ], + systemImageName: "globe") + } +} +#endif diff --git a/DuckDuckGo/VPNShortcutIntents.swift b/DuckDuckGo/VPNShortcutIntents.swift new file mode 100644 index 0000000000..e4d02abff5 --- /dev/null +++ b/DuckDuckGo/VPNShortcutIntents.swift @@ -0,0 +1,118 @@ +// +// VPNShortcutIntents.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 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 { + + private enum DisableAttemptFailure: CustomNSError { + case cancelled + } + + static let title: LocalizedStringResource = "Disable DuckDuckGo VPN" + static let description: LocalizedStringResource = "Disable 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: .vpnShortcutDisconnectAttempt) + + let controller = VPNWidgetTunnelController() + try await controller.stop() + + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() + + DailyPixel.fireDailyAndCount(pixel: .vpnShortcutDisconnectSuccess) + return .result(dialog: IntentDialog(stringLiteral: UserText.vpnControlWidgetDisableVPNIntentSuccess)) + } catch VPNWidgetTunnelController.StopFailure.vpnNotConfigured { + DailyPixel.fireDailyAndCount(pixel: .vpnShortcutDisconnectCancelled) + throw VPNWidgetTunnelController.StopFailure.vpnNotConfigured + } catch { + DailyPixel.fireDailyAndCount(pixel: .vpnShortcutDisconnectFailure, 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: ForegroundContinuableIntent { + static let title: LocalizedStringResource = "Enable DuckDuckGo VPN" + static let description: LocalizedStringResource = "Enable 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: .vpnShortcutConnectAttempt) + + let controller = VPNWidgetTunnelController() + try await controller.start() + + await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() + + DailyPixel.fireDailyAndCount(pixel: .vpnShortcutConnectSuccess) + return .result(dialog: IntentDialog(stringLiteral: UserText.vpnControlWidgetEnableVPNIntentSuccess)) + } catch { + switch error { + case VPNWidgetTunnelController.StartFailure.vpnNotConfigured, + // On update the VPN configuration becomes disabled, until started manually from + // the app. + NEVPNError.configurationDisabled: + + DailyPixel.fireDailyAndCount(pixel: .vpnShortcutConnectCancelled) + + let dialog = IntentDialog(stringLiteral: UserText.vpnNeedsToBeEnabledFromApp) + throw needsToContinueInForegroundError(dialog) { + await UIApplication.shared.open(AppDeepLinkSchemes.openVPN.url) + } + default: + DailyPixel.fireDailyAndCount(pixel: .vpnShortcutConnectFailure, error: error) + + throw error + } + } + } +} 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/WidgetEducationView.swift b/DuckDuckGo/WidgetEducationView.swift deleted file mode 100644 index 81095e78b8..0000000000 --- a/DuckDuckGo/WidgetEducationView.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// WidgetEducationView.swift -// DuckDuckGo -// -// Copyright © 2022 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI -import DesignResourcesKit -import Core - -extension Font { - init(uiFont: UIFont) { - self = Font(uiFont as CTFont) - } -} - -struct WidgetEducationImageConfig { - let image: Image - let maxWidth: CGFloat - let horizontalOffset: CGFloat - - init(image: Image, maxWidth: CGFloat, horizontalOffset: CGFloat = 0) { - self.image = image - self.maxWidth = maxWidth - self.horizontalOffset = horizontalOffset - } -} - -struct WidgetEducationView: View { - typealias ImageConfig = WidgetEducationImageConfig - - let navBarTitle: String - let thirdParagraphText: String - let widgetExampleImageConfig: ImageConfig - - init(navBarTitle: String = UserText.settingsAddWidget, - thirdParagraphText: String = UserText.addWidgetSettingsThirdParagraph, - widgetExampleImageConfig: ImageConfig = .init(image: .widgetExample, maxWidth: Const.Size.imageWidth)) { - self.navBarTitle = navBarTitle - self.thirdParagraphText = thirdParagraphText - self.widgetExampleImageConfig = widgetExampleImageConfig - } - - var body: some View { - ZStack { - Color.background - .ignoresSafeArea() - ScrollView { - VStack(alignment: .leading, spacing: Const.Spacing.paragraph) { - NumberedParagraph(number: 1, - text: Text(UserText.addWidgetSettingsFirstParagraph)) - NumberedParagraph(number: 2, - text: secondParagraphText, - imageConfig: ImageConfig(image: Image.homeScreen, maxWidth: Const.Size.imageWidth)) - NumberedParagraph(number: 3, - text: Text(thirdParagraphText), - imageConfig: widgetExampleImageConfig) - } - .padding(.horizontal) - .padding(.top, Const.Padding.top) - } - } - .navigationBarTitle(navBarTitle, displayMode: .inline) - .onFirstAppear { - Pixel.fire(pixel: .settingsNextStepsAddWidget) - } - } - - private var secondParagraphText: Text { - // https://stackoverflow.com/questions/62168292/what-s-the-equivalent-to-string-localizedstringwithformat-for-swiftuis-lo - Text("addWidget.settings.secondParagraph.\(Image.add)") - } -} - -private struct NumberedParagraph: View { - var number: Int - var text: Text - var imageConfig: WidgetEducationImageConfig? - - var body: some View { - HStack(alignment: .firstTextBaseline, spacing: Const.Spacing.numberAndText) { - NumberedCircle(number: number) - VStack(alignment: .leading, spacing: Const.Spacing.textAndImage) { - text - .font(Font(uiFont: Const.Font.text)) - .lineSpacing(Const.Spacing.line) - .foregroundColor(Color.font) - if let imageConfig { - imageConfig - .image - .resizable() - .scaledToFit() - .frame(maxWidth: imageConfig.maxWidth) - .offset(x: imageConfig.horizontalOffset) - } - } - } - } -} - -private struct NumberedCircle: View { - var number: Int - - var body: some View { - ZStack { - Circle() - .foregroundColor(Color.circle) - Text("\(number)") - .font(Font(uiFont: Const.Font.numbers)) - .foregroundColor(Color.numbers) - } - .frame(width: Const.Size.circle.width, - height: Const.Size.circle.height) - } -} - -private extension Color { - static let background = Color(designSystemColor: .background) - static let font = Color("WidgetEducationFontColor") - static let circle = Color(designSystemColor: .accent) - static let numbers = Color.white -} - -private extension Image { - static let add = Image("WidgetEducationAddIcon") - static let widgetExample = Image("WidgetEducationWidgetExample") - static let homeScreen = Image("WidgetEducationHomeScreen") -} - -private enum Const { - enum Font { - static let text = UIFont.appFont(ofSize: 17) - static let numbers = UIFont.boldAppFont(ofSize: 16) - } - - enum Padding { - static let top: CGFloat = 32 - } - - enum Spacing { - static let paragraph: CGFloat = 24 - static let numberAndText: CGFloat = 16 - static let textAndImage: CGFloat = 16 - static let line: CGFloat = 4 - } - - enum Size { - static let circle = CGSize(width: 24, height: 24) - static let imageWidth: CGFloat = 270 - } -} - -struct WidgetEducationView_Previews: PreviewProvider { - static var previews: some View { - WidgetEducationView() - } -} diff --git a/DuckDuckGo/Widgets/ControlCenterWidget.swift b/DuckDuckGo/Widgets/ControlCenterWidget.swift new file mode 100644 index 0000000000..744ae7c1b1 --- /dev/null +++ b/DuckDuckGo/Widgets/ControlCenterWidget.swift @@ -0,0 +1,33 @@ +// +// ControlCenterWidget.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 SwiftUICore + +/// Our Control Center Widgets +/// +/// This isn't strictly necessary right now, but it allows us an opportunity to write cleaner code in +/// perparation of having more widgets. +/// +enum ControlCenterWidget { + case vpnToggle + + var image: Image { + Image("ControlCenter-VPN-off") + } +} diff --git a/DuckDuckGo/Widgets/Education/ControlCenterWidgetEducationView.swift b/DuckDuckGo/Widgets/Education/ControlCenterWidgetEducationView.swift new file mode 100644 index 0000000000..f5382082c2 --- /dev/null +++ b/DuckDuckGo/Widgets/Education/ControlCenterWidgetEducationView.swift @@ -0,0 +1,112 @@ +// +// ControlCenterWidgetEducationView.swift +// DuckDuckGo +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import DesignResourcesKit +import Core + +@available(iOS 18.0, *) +struct ControlCenterWidgetEducationView: View { + typealias Detail = NumberedParagraphConfig.Detail + + enum Padding { + static let top: CGFloat = 24 + static let horizontal: CGFloat = 24 + static let widgetBorder: CGFloat = 20 + } + + enum Spacing { + static let titleAndList: CGFloat = 24 + } + + enum Size { + static let exampleImageWidth: CGFloat = 270 + static let widgetHeight: CGFloat = 30 + static let widgetWidth: CGFloat = 30 + static let widgetMaxWidth: CGFloat = 70 // width + padding * 2 + } + + @Environment(\.dismiss) private var dismiss + + let navBarTitle: String + let widgetIconDetail: Detail + + init(navBarTitle: String, widget: ControlCenterWidget) { + + self.navBarTitle = navBarTitle + + let icon = widget.image + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fill) + .foregroundStyle(Color.white) + .frame(width: Size.widgetWidth, height: Size.widgetHeight) + .padding(Padding.widgetBorder) + .background(Circle().fill(Color.controlWidgetBackground)) + .frame(maxWidth: Size.widgetMaxWidth) + + self.widgetIconDetail = .view(AnyView(icon), maxWidth: Size.widgetMaxWidth) + } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: Spacing.titleAndList) { + Text(navBarTitle) + .font(.system(size: 22, weight: .bold, design: .default)) + + NumberedParagraphListView( + paragraphConfig: [ + NumberedParagraphConfig(text: UserText.controlCenterWidgetEducationParagraph1), + NumberedParagraphConfig(text: UserText.controlCenterWidgetEducationParagraph2), + NumberedParagraphConfig( + text: UserText.controlCenterWidgetEducationParagraph3, + detail: .image(Image.controlCenterBottom, + maxWidth: Size.exampleImageWidth)), + NumberedParagraphConfig( + text: UserText.controlCenterVPNWidgetEducationParagraph, + detail: widgetIconDetail) + ] + ) + .foregroundColor(Color.font) + } + .padding(.horizontal, Padding.horizontal) + .padding(.top, Padding.top) + } + .navigationBarTitle("") + .background(Color.background) + } +} + +private extension Color { + static let background = Color(designSystemColor: .background) + static let controlWidgetBackground = Color("controlWidgetBackground", bundle: DesignResourcesKit.bundle) + static let font = Color("WidgetEducationFontColor") +} + +private extension Image { + static let controlCenterBottom = Image("ControlCenterBottom") +} + + +@available(iOS 18.0, *) +struct ControlCenterWidgetEducationView_Previews: PreviewProvider { + static var previews: some View { + ControlCenterWidgetEducationView(navBarTitle: "Control Center", widget: .vpnToggle) + } +} diff --git a/DuckDuckGo/Widgets/Education/NumberedParagraphView/NumberedParagraphView.swift b/DuckDuckGo/Widgets/Education/NumberedParagraphView/NumberedParagraphView.swift new file mode 100644 index 0000000000..1a90183fb9 --- /dev/null +++ b/DuckDuckGo/Widgets/Education/NumberedParagraphView/NumberedParagraphView.swift @@ -0,0 +1,164 @@ +// +// NumberedParagraphView.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 SwiftUI +import DesignResourcesKit +import Core + +extension Font { + init(uiFont: UIFont) { + self = Font(uiFont as CTFont) + } +} + +struct NumberedParagraphListView: View { + let spacing: CGFloat? + let paragraphConfig: [NumberedParagraphConfig] + + init(spacing: CGFloat? = nil, + paragraphConfig: [NumberedParagraphConfig]) { + + self.paragraphConfig = paragraphConfig + self.spacing = spacing ?? Const.Spacing.paragraph + } + + var body: some View { + VStack(spacing: spacing) { + ForEach(paragraphConfig.indices, id: \.self) { index in + NumberedParagraphView(number: index + 1, + config: paragraphConfig[index]) + } + } + } +} + +struct NumberedParagraphConfig { + enum Detail { + case image(_ image: Image, + maxWidth: CGFloat, + horizontalOffset: CGFloat = 0, + dropsShadow: Bool = false) + case view(_ view: AnyView, + maxWidth: CGFloat) + } + + let text: Text + let detail: Detail? + + init(text: String, detail: Detail? = nil) { + // The LocalizedStringKey wrapper is necessary to properly parse markdown + self.text = Text(LocalizedStringKey(text)) + self.detail = detail + } + + init(text: Text, detail: Detail? = nil) { + self.text = text + self.detail = detail + } +} + +private struct NumberedParagraphView: View { + let number: Int + let config: NumberedParagraphConfig + + var body: some View { + HStack(alignment: .firstTextBaseline, spacing: Const.Spacing.numberAndText) { + NumberedCircle(number: number) + VStack(alignment: .leading, spacing: Const.Spacing.textAndImage) { + HStack { + config.text + .foregroundStyle(Color(designSystemColor: .textPrimary)) + .font(Font(uiFont: Const.Font.text)) + .lineSpacing(Const.Spacing.line) + + Spacer() + } + + if let detailConfig = config.detail { + switch detailConfig { + case .image(let image, let maxWidth, let horizontalOffset, let dropsShadow): + image + .resizable() + .scaledToFit() + .frame(maxWidth: maxWidth) + .offset(x: horizontalOffset) + .if(dropsShadow) { view in + view.shadow(color: .black.opacity(0.08), radius: 8, x: 0, y: 8) + .shadow(color: .black.opacity(0.1), radius: 3, x: 0, y: 2) + } + case .view(let view, let maxWidth): + view + .scaledToFit() + .frame(maxWidth: maxWidth) + } + + } + } + } + } +} + +private struct NumberedCircle: View { + var number: Int + + var body: some View { + ZStack { + Circle() + .foregroundColor(Color.circle) + Text("\(number)") + .font(Font(uiFont: Const.Font.numbers)) + .foregroundColor(Color.numbers) + } + .frame(width: Const.Size.circle.width, + height: Const.Size.circle.height) + } +} + +private extension Color { + static let circle = Color(designSystemColor: .textSelectionFill) + static let numbers = Color(designSystemColor: .textLink) +} + +private enum Const { + enum Font { + static let text = UIFont.appFont(ofSize: 17) + static let numbers = UIFont.boldAppFont(ofSize: 16) + } + + enum Spacing { + static let paragraph: CGFloat = 24 + static let numberAndText: CGFloat = 16 + static let textAndImage: CGFloat = 16 + static let line: CGFloat = 4 + } + + enum Size { + static let circle = CGSize(width: 24, height: 24) + } +} + +@available(iOS 17.0, *) +struct NumberedParagraphListView_Previews: PreviewProvider { + static var previews: some View { + NumberedParagraphListView(paragraphConfig: [ + NumberedParagraphConfig(text: "Hellow world"), + NumberedParagraphConfig(text: "Paragraph 2"), + ]) + } +} diff --git a/DuckDuckGo/Widgets/Education/WidgetEducationView.swift b/DuckDuckGo/Widgets/Education/WidgetEducationView.swift new file mode 100644 index 0000000000..e48dc6f819 --- /dev/null +++ b/DuckDuckGo/Widgets/Education/WidgetEducationView.swift @@ -0,0 +1,121 @@ +// +// WidgetEducationView.swift +// DuckDuckGo +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import DesignResourcesKit +import Core + +struct WidgetEducationView: View { + typealias Detail = NumberedParagraphConfig.Detail + + @Environment(\.dismiss) private var dismiss + + let navBarTitle: String + let thirdParagraphText: String + let thirdParagraphDetail: Detail + + var secondParagraphText: Text { + if #available(iOS 18, *) { + // The LocalizedStringKey wrapper is necessary to properly parse markdown + return Text(LocalizedStringKey(UserText.addWidgetSettingsSecondParagraph)) + } else { + // Ref: https://stackoverflow.com/questions/62168292/what-s-the-equivalent-to-string-localizedstringwithformat-for-swiftuis-lo + return Text("addWidget.settings.secondParagraph.\(Image.add)") + } + } + + init(navBarTitle: String = UserText.settingsAddWidget, + thirdParagraphText: String = UserText.addWidgetSettingsThirdParagraph, + thirdParagraphDetail: Detail = .image(Image.widgetExample, maxWidth: Const.Size.exampleImageWidth)) { + + self.navBarTitle = navBarTitle + self.thirdParagraphText = thirdParagraphText + self.thirdParagraphDetail = thirdParagraphDetail + } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: Const.Spacing.titleAndList) { + Text(navBarTitle) + .font(.system(size: 22, weight: .bold, design: .default)) + + NumberedParagraphListView( + paragraphConfig: [ + NumberedParagraphConfig(text: UserText.addWidgetSettingsFirstParagraph), + NumberedParagraphConfig( + text: secondParagraphText, + detail: .image(Image.homeScreen, + maxWidth: Const.Size.exampleImageWidth)), + NumberedParagraphConfig( + text: thirdParagraphText, + detail: thirdParagraphDetail) + ] + ) + .foregroundColor(Color.font) + } + .padding(.horizontal, Const.Padding.horizontal) + .padding(.top, Const.Padding.top) + } + .navigationBarTitle("") + .background(Color.background) + .onFirstAppear { + Pixel.fire(pixel: .settingsNextStepsAddWidget) + } + } +} + +private extension Color { + static let background = Color(designSystemColor: .background) + static let font = Color("WidgetEducationFontColor") +} + +extension Image { + static let add = Image("WidgetEducationAddIcon") + static let widgetExample = Image("WidgetEducationWidgetExample") + static var homeScreen: Image { + if #available(iOS 18, *) { + Image("WidgetEducationHomeScreen") + } else { + Image("WidgetEducationHomeScreeniOS17") + } + } +} + +private enum Const { + + enum Padding { + static let top: CGFloat = 24 + static let horizontal: CGFloat = 24 + } + + enum Spacing { + static let titleAndList: CGFloat = 24 + } + + enum Size { + static let exampleImageWidth: CGFloat = 270 + } +} + +@available(iOS 17.0, *) +struct WidgetEducationView_Previews: PreviewProvider { + static var previews: some View { + WidgetEducationView() + } +} diff --git a/DuckDuckGo/WidgetEducationViewController.swift b/DuckDuckGo/Widgets/Education/WidgetEducationViewController.swift similarity index 100% rename from DuckDuckGo/WidgetEducationViewController.swift rename to DuckDuckGo/Widgets/Education/WidgetEducationViewController.swift diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 022fc631d0..c3a555ae31 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -131,13 +131,16 @@ "addWidget.description" = "Get quick access to private search and the sites you love."; /* No comment provided by engineer. */ -"addWidget.settings.firstParagraph" = "Long-press on the Home Screen to enter jiggle mode."; +"addWidget.settings.firstParagraph" = "Long-press anywhere on the Home Screen until the apps jiggle"; + +/* No comment provided by engineer. */ +"addWidget.settings.secondParagraph" = "Tap the **Edit** button, then choose **Add Widget**"; /* Replacement string is a plus button icon. */ "addWidget.settings.secondParagraph.%@" = "Tap the plus %@ button."; /* No comment provided by engineer. */ -"addWidget.settings.title" = "Find and select DuckDuckGo. Then choose a widget."; +"addWidget.settings.title" = "Find and select **DuckDuckGo**. Then choose a widget."; /* No comment provided by engineer. */ "addWidget.title" = "One tap to your favorite sites."; @@ -876,6 +879,18 @@ /* Title for a button that triggers an unknown search query for the user. */ "contextual.onboarding.try-search.surprise-me-title" = "Surprise me!"; +/* Fourth paragraph of the Control Center VPN Widget Education screen */ +"control.center.vpn.widget.education.paragraph" = "Find **DuckDuckGo** in the list, then tap **VPN control** to add it"; + +/* First paragraph of the Control Center Widget Education screen */ +"control.center.widget.education.paragraph.1" = "Swipe down from the top-right corner of the screen"; + +/* Second paragraph of the Control Center Widget Education screen */ +"control.center.widget.education.paragraph.2" = "Tap the **+** button at the top left of **Control Center**"; + +/* Third paragraph of the Control Center Widget Education screen */ +"control.center.widget.education.paragraph.3" = "Tap **Add a Control** at the bottom of the screen"; + /* Crash Report always send button title */ "crash.report.dialog.always.send" = "Always Send Crash Reports"; @@ -1848,6 +1863,9 @@ https://duckduckgo.com/mac"; /* Title for the VPN Settings screen. */ "network.protection.vpn.settings.title" = "VPN Settings"; +/* Section header for the shortcuts section in VPN settings. */ +"network.protection.vpn.shortcuts.section.header" = "Shortcuts"; + /* Information message about New Tab Page redesign */ "new.tab.page.intro.message.body" = "Customize your Favorites and go-to features. Reorder things or hide them to keep it clean."; @@ -2186,6 +2204,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Settings screen cell text for add widget to the home screen */ "settings.add.widget" = "Add Widget to Home Screen"; +/* Title for the settings VPN subsection where the user can learn how to add the VPN to their home screen */ +"settings.add.widget.vpn" = "Add DuckDuckGo VPN Widget to Your Home Screen"; + /* Name of the settings subsection related to the address bar Settings screen cell text for addess bar position */ "settings.address.bar" = "Address Bar"; @@ -2837,7 +2858,28 @@ But if you *do* want a peek under the hood, you can find more information about "vpn.access-revoked.alert.title" = "VPN disconnected due to expired subscription"; /* Title for the VPN widget onboarding screen */ -"vpn.addWidget.settings.title" = "Find and select DuckDuckGo. Then swipe to VPN and select Add Widget."; +"vpn.addWidget.settings.title" = "Find and select **DuckDuckGo**, then swipe to **VPN** and select **Add Widget**"; + +/* Message for success when running the intent to enable the DuckDuckGo VPN */ +"vpn.control-widget.disable.vpn.intent.success" = "DuckDuckGo VPN is connected"; + +/* No comment provided by engineer. */ +"vpn.control-widget.education.screen.description" = "Siri Shortcuts let you connect to your VPN with a quick voice command, like “Siri, turn on DuckDuckGo VPN”. It’s a fast, hands-free way to use your VPN."; + +/* Siri commands education screen: example 1 */ +"vpn.control-widget.education.screen.example1" = "Siri, start DuckDuckGo VPN."; + +/* Siri commands education screen: example 2 */ +"vpn.control-widget.education.screen.example2" = "Siri, enable DuckDuckGo VPN."; + +/* Siri commands education screen: example 3 */ +"vpn.control-widget.education.screen.example3" = "Siri, protect my device with DuckDuckGo."; + +/* Title for the VPN Control Widget education screen title */ +"vpn.control-widget.education.screen.title" = "Control DuckDuckGo VPN with Siri!"; + +/* Message for success when running the intent to enable the DuckDuckGo VPN */ +"vpn.control-widget.enable.vpn.intent.success" = "DuckDuckGo VPN is connected"; /* Title for the Cancel button of the VPN feedback form */ "vpn.feedback-form.button.cancel" = "Cancel"; @@ -2908,9 +2950,15 @@ But if you *do* want a peek under the hood, you can find more information about /* Title for each screen of the VPN feedback form */ "vpn.feedback-form.title" = "Help Improve the DuckDuckGo VPN"; +/* VPN settings screen cell text for adding the VPN widget to the control center */ +"vpn.settings.add.control-center.widget" = "Add VPN to Control Center"; + /* VPN settings screen cell text for adding the VPN widget to the home screen */ "vpn.settings.add.widget" = "Add VPN Widget to Home Screen"; +/* VPN settings screen cell text to learn how to control the VPN with Siri */ +"vpn.settings.control.with.siri" = "Control VPN With Siri"; + /* Disclaimer for the DNS Server section on the DNS Server screen */ "vpn.settings.dns.section-disclaimer" = "Using a custom DNS server can impact browsing speeds and expose your activity to third parties if the server isn't secure or reliable."; diff --git a/LocalPackages/AIChat/Package.swift b/LocalPackages/AIChat/Package.swift index f54439b680..7c53f99d29 100644 --- a/LocalPackages/AIChat/Package.swift +++ b/LocalPackages/AIChat/Package.swift @@ -14,7 +14,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.3.0") + .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.3.1") ], targets: [ .target( diff --git a/LocalPackages/DuckUI/Sources/DuckUI/Color.swift b/LocalPackages/DuckUI/Sources/DuckUI/Color.swift index 4b7c9fee5a..b869cd6d89 100644 --- a/LocalPackages/DuckUI/Sources/DuckUI/Color.swift +++ b/LocalPackages/DuckUI/Sources/DuckUI/Color.swift @@ -44,8 +44,8 @@ public extension Color { static let blue30 = Color.init(0x7295F6) static let blue20 = Color.init(0x8FABF9) static let blue10 = Color.init(0xADC2FC) - static let blue0 = Color.init(0xADC2FC) - + static let blue0 = Color.init(0xCCDAFF) + static let gray95 = Color.init(0x111111) static let gray90 = Color.init(0x222222) static let gray85 = Color.init(0x333333) diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 1d00f55394..21cb7ba29d 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -33,7 +33,7 @@ let package = Package( ], dependencies: [ .package(path: "../DuckUI"), - .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.3.0"), + .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.3.1"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.2"), ], targets: [ 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..007b7ea741 --- /dev/null +++ b/LocalPackages/VPNiOS/Package.swift @@ -0,0 +1,24 @@ +// 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"]), + ], + 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" + ), + ] +) diff --git a/LocalPackages/VPNiOS/Sources/VPNWidgetSupport/VPNWidgetTunnelController.swift b/LocalPackages/VPNiOS/Sources/VPNWidgetSupport/VPNWidgetTunnelController.swift new file mode 100644 index 0000000000..252fbce78e --- /dev/null +++ b/LocalPackages/VPNiOS/Sources/VPNWidgetSupport/VPNWidgetTunnelController.swift @@ -0,0 +1,85 @@ +// +// VPNWidgetTunnelController.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 { + + let start = Date() + + while true { + try await Task.sleep(for: .milliseconds(500)) + + if abs(start.timeIntervalSinceNow) > 30 + || (manager.connection.status != .connecting && manager.connection.status != .disconnecting) { + + break + } + } + } +} diff --git a/LocalPackages/Waitlist/Package.swift b/LocalPackages/Waitlist/Package.swift index 13b5954238..8fdebe4303 100644 --- a/LocalPackages/Waitlist/Package.swift +++ b/LocalPackages/Waitlist/Package.swift @@ -15,7 +15,7 @@ let package = Package( targets: ["Waitlist", "WaitlistMocks"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.3.0"), + .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.3.1"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "3.1.2"), ], targets: [ diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 30c02830ee..7f75a8b758 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -532,7 +532,8 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { activationDateStore.setActivationDateIfNecessary() activationDateStore.updateLastActiveDate() - WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") + + VPNReloadStatusWidgets() } private static func entitlementCheck(accountManager: AccountManager) async -> Result { diff --git a/Widgets/ControlWidgetVPNIntents.swift b/Widgets/ControlWidgetVPNIntents.swift new file mode 100644 index 0000000000..3525a45094 --- /dev/null +++ b/Widgets/ControlWidgetVPNIntents.swift @@ -0,0 +1,111 @@ +// +// ControlWidgetVPNIntents.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 +import OSLog +import VPNWidgetSupport + +// MARK: - Toggle + +@available(iOS 17.0, *) +struct ControlWidgetToggleVPNIntent: SetValueIntent { + + private enum EnableAttemptFailure: CustomNSError, LocalizedError { + case cancelled + + var errorDescription: String? { + switch self { + case .cancelled: + return UserText.vpnNeedsToBeEnabledFromApp + } + } + } + + 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, + // On update the VPN configuration becomes disabled, until started manually from + // the app. + NEVPNError.configurationDisabled: + + DailyPixel.fireDailyAndCount(pixel: .vpnControlCenterConnectCancelled) + throw EnableAttemptFailure.cancelled + 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/VPNControlStatusValueProvider.swift b/Widgets/VPNControlStatusValueProvider.swift new file mode 100644 index 0000000000..b5d3ba1c7a --- /dev/null +++ b/Widgets/VPNControlStatusValueProvider.swift @@ -0,0 +1,36 @@ +// +// VPNControlStatusValueProvider.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 +import NetworkExtension +import WidgetKit + +struct VPNControlStatusValueProvider: ControlValueProvider { + + let previewValue: VPNStatus = .notConfigured + + func currentValue() async throws -> VPNStatus { + guard let manager = try await NETunnelProviderManager.loadAllFromPreferences().first else { + + return .notConfigured + } + + return .status(manager.connection.status) + } +} diff --git a/Widgets/VPNControlWidget.swift b/Widgets/VPNControlWidget.swift new file mode 100644 index 0000000000..4b1048147b --- /dev/null +++ b/Widgets/VPNControlWidget.swift @@ -0,0 +1,48 @@ +// +// VPNControlWidget.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 +import SwiftUI +import WidgetKit + +#if ALPHA || DEBUG +@available(iOSApplicationExtension 18.0, *) +public struct VPNControlWidget: ControlWidget { + static let displayName = LocalizedStringResource(stringLiteral: "DuckDuckGo\nVPN") + static let description = LocalizedStringResource(stringLiteral: "View and manage your VPN connection. Requires a Privacy Pro subscription.") + + public init() {} + + public var body: some ControlWidgetConfiguration { + StaticControlConfiguration(kind: .vpn, + provider: VPNControlStatusValueProvider()) { status in + + ControlWidgetToggle("DuckDuckGo\nVPN", isOn: status.isConnected, action: ControlWidgetToggleVPNIntent()) { isOn in + if isOn { + Label("Connected", image: "ControlCenter-VPN-on") + } else { + Label("Not Connected", image: "ControlCenter-VPN-off") + } + } + .tint(.green) + }.displayName(Self.displayName) + .description(Self.description) + } +} +#endif diff --git a/Widgets/VPNWidget.swift b/Widgets/VPNWidget.swift index 9ad909410f..db9107220e 100644 --- a/Widgets/VPNWidget.swift +++ b/Widgets/VPNWidget.swift @@ -30,6 +30,15 @@ enum VPNStatus { case status(NEVPNStatus) case error case notConfigured + + var isConnected: Bool { + switch self { + case .status(let status): + return status.isConnected + default: + return false + } + } } struct VPNStatusTimelineEntry: TimelineEntry { @@ -171,7 +180,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) .borderedStyle(widgetRenderingMode == .fullColor) @@ -190,7 +199,7 @@ struct VPNStatusView: View { .padding(.top, 6) .padding(.bottom, 16) case .connecting, .reasserting: - Button(UserText.vpnWidgetDisconnectButton, intent: DisableVPNIntent()) + Button(UserText.vpnWidgetDisconnectButton, intent: WidgetDisableVPNIntent()) .borderedStyle(widgetRenderingMode == .fullColor) .makeAccentable(status == .connected) .font(.system(size: 14, weight: .semibold)) @@ -234,7 +243,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) @@ -297,10 +306,8 @@ struct VPNStatusView: View { @available(iOSApplicationExtension 17.0, *) struct VPNStatusWidget: Widget { - let kind: String = "VPNStatusWidget" - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: VPNStatusTimelineProvider()) { entry in + StaticConfiguration(kind: WidgetKind.vpn.rawValue, provider: VPNStatusTimelineProvider()) { entry in VPNStatusView(entry: entry).widgetURL(DeepLinks.openVPN) } .configurationDisplayName(UserText.vpnWidgetGalleryDisplayName) diff --git a/Widgets/WidgetKind.swift b/Widgets/WidgetKind.swift new file mode 100644 index 0000000000..2c388c5855 --- /dev/null +++ b/Widgets/WidgetKind.swift @@ -0,0 +1,62 @@ +// +// WidgetKind.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 +import SwiftUI +import WidgetKit + +enum WidgetKind: String, Codable { + case vpn = "VPNStatusWidget" +} + +enum ControlWidgetKind: String, Codable { + case vpn = "VPNControlWidget" +} + +extension WidgetCenter { + func reloadTimelines(ofKind kind: WidgetKind) { + reloadTimelines(ofKind: kind.rawValue) + } +} + +@available(iOS 18.0, *) +extension ControlCenter { + func reloadControls(ofKind kind: ControlWidgetKind) { + reloadControls(ofKind: kind.rawValue) + } +} + +@available(iOS 18.0, *) +extension StaticControlConfiguration { + @MainActor @preconcurrency + init(kind: ControlWidgetKind, + provider: Provider, + @ControlWidgetTemplateBuilder content: @escaping (Provider.Value) -> Content) + where Provider: ControlValueProvider { + self.init(kind: kind.rawValue, provider: provider, content: content) + } +} + +func VPNReloadStatusWidgets() { + WidgetCenter.shared.reloadTimelines(ofKind: .vpn) + + if #available(iOS 18.0, *) { + ControlCenter.shared.reloadControls(ofKind: .vpn) + } +} diff --git a/DuckDuckGo/VPNIntents.swift b/Widgets/WidgetVPNIntents.swift similarity index 54% rename from DuckDuckGo/VPNIntents.swift rename to Widgets/WidgetVPNIntents.swift index 21394de016..1d675e2c98 100644 --- a/DuckDuckGo/VPNIntents.swift +++ b/Widgets/WidgetVPNIntents.swift @@ -1,5 +1,5 @@ // -// VPNIntents.swift +// WidgetVPNIntents.swift // DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. @@ -22,99 +22,102 @@ 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 DisableVPNIntent: AppIntent { +struct WidgetDisableVPNIntent: 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 var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication @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() - } - - manager.isOnDemandEnabled = false - try await manager.saveToPreferences() - manager.connection.stopVPNTunnel() + let controller = VPNWidgetTunnelController() + try await controller.stop() - WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() - 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 - } + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectSuccess) + return .result() + } catch VPNWidgetTunnelController.StopFailure.vpnNotConfigured, + NEVPNError.configurationDisabled { + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetDisconnectCancelled) return .result() } catch { - return .result() + 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 EnableVPNIntent: AppIntent { +struct WidgetEnableVPNIntent: AppIntent { + + private enum EnableAttemptFailure: CustomNSError, LocalizedError { + case cancelled - static let title: LocalizedStringResource = "Enable VPN" + var errorDescription: String? { + switch self { + case .cancelled: + return UserText.vpnNeedsToBeEnabledFromApp + } + } + } + + 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.fire(pixel: .networkProtectionWidgetConnectAttempt) + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectAttempt) - let managers = try await NETunnelProviderManager.loadAllFromPreferences() - guard let manager = managers.first else { - return .result() - } + let controller = VPNWidgetTunnelController() + try await controller.start() - manager.isOnDemandEnabled = true - try await manager.saveToPreferences() - try manager.connection.startVPNTunnel() - - WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") await VPNSnoozeLiveActivityManager().endSnoozeActivity() + VPNReloadStatusWidgets() - 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 - } - + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectSuccess) return .result() } catch { - return .result() + switch error { + case VPNWidgetTunnelController.StartFailure.vpnNotConfigured, + NEVPNError.configurationDisabled: + + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectCancelled) + throw EnableAttemptFailure.cancelled + default: + DailyPixel.fireDailyAndCount(pixel: .networkProtectionWidgetConnectFailure, error: error) + throw error + } } } - } // MARK: - Snooze @@ -136,7 +139,7 @@ struct CancelSnoozeVPNIntent: AppIntent { } try? await session.sendProviderMessage(.cancelSnooze) - WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") + VPNReloadStatusWidgets() await VPNSnoozeLiveActivityManager().endSnoozeActivity() return .result() @@ -144,7 +147,6 @@ struct CancelSnoozeVPNIntent: AppIntent { return .result() } } - } @available(iOS 17.0, *) @@ -162,7 +164,7 @@ struct CancelSnoozeLiveActivityAppIntent: LiveActivityIntent { try? await session.sendProviderMessage(.cancelSnooze) await VPNSnoozeLiveActivityManager().endSnoozeActivity() - WidgetCenter.shared.reloadTimelines(ofKind: "VPNStatusWidget") + VPNReloadStatusWidgets() return .result() } diff --git a/Widgets/Widgets.swift b/Widgets/Widgets.swift index 8e545bb352..6966e69c21 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 @@ -257,6 +258,12 @@ struct VPNBundle: WidgetBundle { VPNStatusWidget() VPNSnoozeLiveActivity() } + + #if ALPHA || DEBUG + if #available(iOS 18, *) { + VPNControlWidget() + } + #endif } } diff --git a/Widgets/WidgetsShared.xcassets/Contents.json b/Widgets/WidgetsShared.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Widgets/WidgetsShared.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Widgets/WidgetsShared.xcassets/SFSymbols/Contents.json b/Widgets/WidgetsShared.xcassets/SFSymbols/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Widgets/WidgetsShared.xcassets/SFSymbols/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/Contents.json b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/Contents.json new file mode 100644 index 0000000000..ca0236fb00 --- /dev/null +++ b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-off.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "VPNOFF4.svg", + "idiom" : "universal" + } + ] +} 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 new file mode 100644 index 0000000000..7bc2896a16 --- /dev/null +++ b/Widgets/WidgetsShared.xcassets/SFSymbols/ControlCenter-VPN-on.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "VPNON4.svg", + "idiom" : "universal" + } + ] +} 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.";