diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7f1aef9234..c270aa271d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1785,6 +1785,10 @@ 7B1E819E27C8874900FF0E60 /* ContentOverlayPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */; }; 7B1E819F27C8874900FF0E60 /* ContentOverlay.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */; }; 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */; }; + 7B22D86E2CCFD7B7006A76E1 /* TipKitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B22D86D2CCFD7B7006A76E1 /* TipKitController.swift */; }; + 7B22D86F2CCFD7B7006A76E1 /* TipKitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B22D86D2CCFD7B7006A76E1 /* TipKitController.swift */; }; + 7B22D8702CCFD7B7006A76E1 /* TipKitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B22D86D2CCFD7B7006A76E1 /* TipKitController.swift */; }; + 7B22D8712CCFD7B7006A76E1 /* TipKitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B22D86D2CCFD7B7006A76E1 /* TipKitController.swift */; }; 7B2366842C09FAC2002D393F /* VPNAppLauncher in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2366832C09FAC2002D393F /* VPNAppLauncher */; }; 7B2366862C09FACD002D393F /* VPNAppLauncher in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2366852C09FACD002D393F /* VPNAppLauncher */; }; 7B2366882C09FADA002D393F /* VPNAppLauncher in Frameworks */ = {isa = PBXBuildFile; productRef = 7B2366872C09FADA002D393F /* VPNAppLauncher */; }; @@ -1872,8 +1876,8 @@ 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; 7BAF9E4D2A8A3CCB002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; 7BB108592A43375D000AB95F /* PFMoveApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BB108582A43375D000AB95F /* PFMoveApplication.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; - 7BB4BC632C5BC13D00E06FC8 /* SiteTroubleshootingInfoPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB4BC622C5BC13D00E06FC8 /* SiteTroubleshootingInfoPublisher.swift */; }; - 7BB4BC642C5BC13D00E06FC8 /* SiteTroubleshootingInfoPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB4BC622C5BC13D00E06FC8 /* SiteTroubleshootingInfoPublisher.swift */; }; + 7BB4BC632C5BC13D00E06FC8 /* ActiveSiteInfoPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB4BC622C5BC13D00E06FC8 /* ActiveSiteInfoPublisher.swift */; }; + 7BB4BC642C5BC13D00E06FC8 /* ActiveSiteInfoPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB4BC622C5BC13D00E06FC8 /* ActiveSiteInfoPublisher.swift */; }; 7BB4BC6A2C5CD96200E06FC8 /* ActiveDomainPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB4BC692C5CD96200E06FC8 /* ActiveDomainPublisher.swift */; }; 7BB4BC6B2C5CD96200E06FC8 /* ActiveDomainPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB4BC692C5CD96200E06FC8 /* ActiveDomainPublisher.swift */; }; 7BBD45B12A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; @@ -1889,6 +1893,22 @@ 7BD7B0042C19D3830039D20A /* VPNIPCResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD7B0002C19D3830039D20A /* VPNIPCResources.swift */; }; 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; + 7BDBAD142CBFF633000379B7 /* TipKitAppEventHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD112CBFF633000379B7 /* TipKitAppEventHandling.swift */; }; + 7BDBAD152CBFF633000379B7 /* TipKitController+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD122CBFF633000379B7 /* TipKitController+ConvenienceInitializers.swift */; }; + 7BDBAD162CBFF633000379B7 /* TipKitDebugOptionsUIActionHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD132CBFF633000379B7 /* TipKitDebugOptionsUIActionHandling.swift */; }; + 7BDBAD172CBFF633000379B7 /* Logger+TipKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD102CBFF633000379B7 /* Logger+TipKit.swift */; }; + 7BDBAD182CBFF633000379B7 /* TipKitAppEventHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD112CBFF633000379B7 /* TipKitAppEventHandling.swift */; }; + 7BDBAD192CBFF633000379B7 /* TipKitController+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD122CBFF633000379B7 /* TipKitController+ConvenienceInitializers.swift */; }; + 7BDBAD1A2CBFF633000379B7 /* TipKitDebugOptionsUIActionHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD132CBFF633000379B7 /* TipKitDebugOptionsUIActionHandling.swift */; }; + 7BDBAD1B2CBFF633000379B7 /* Logger+TipKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD102CBFF633000379B7 /* Logger+TipKit.swift */; }; + 7BDBAD1C2CBFF95F000379B7 /* TipKitController+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD122CBFF633000379B7 /* TipKitController+ConvenienceInitializers.swift */; }; + 7BDBAD1D2CBFF95F000379B7 /* TipKitController+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD122CBFF633000379B7 /* TipKitController+ConvenienceInitializers.swift */; }; + 7BDBAD1E2CBFF96A000379B7 /* Logger+TipKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD102CBFF633000379B7 /* Logger+TipKit.swift */; }; + 7BDBAD1F2CBFF96A000379B7 /* Logger+TipKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD102CBFF633000379B7 /* Logger+TipKit.swift */; }; + 7BDBAD202CBFF977000379B7 /* TipKitAppEventHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD112CBFF633000379B7 /* TipKitAppEventHandling.swift */; }; + 7BDBAD212CBFF977000379B7 /* TipKitAppEventHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD112CBFF633000379B7 /* TipKitAppEventHandling.swift */; }; + 7BDBAD222CBFF97E000379B7 /* TipKitDebugOptionsUIActionHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD132CBFF633000379B7 /* TipKitDebugOptionsUIActionHandling.swift */; }; + 7BDBAD232CBFF97E000379B7 /* TipKitDebugOptionsUIActionHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDBAD132CBFF633000379B7 /* TipKitDebugOptionsUIActionHandling.swift */; }; 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; 7BE146082A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; 7BEC182F2AD5D8DC00D30536 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */; }; @@ -4095,6 +4115,7 @@ 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayPopover.swift; sourceTree = ""; }; 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ContentOverlay.storyboard; sourceTree = ""; }; 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayViewController.swift; sourceTree = ""; }; + 7B22D86D2CCFD7B7006A76E1 /* TipKitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitController.swift; sourceTree = ""; }; 7B25FE322AD12C990012AFAB /* NetworkProtectionMac */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = NetworkProtectionMac; sourceTree = ""; }; 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAppEvents.swift; sourceTree = ""; }; 7B2E52242A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAgentNotificationsPresenter.swift; sourceTree = ""; }; @@ -4139,7 +4160,7 @@ 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionIPCTunnelController.swift; sourceTree = ""; }; 7BB108572A43375D000AB95F /* PFMoveApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMoveApplication.h; sourceTree = ""; }; 7BB108582A43375D000AB95F /* PFMoveApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMoveApplication.m; sourceTree = ""; }; - 7BB4BC622C5BC13D00E06FC8 /* SiteTroubleshootingInfoPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteTroubleshootingInfoPublisher.swift; sourceTree = ""; }; + 7BB4BC622C5BC13D00E06FC8 /* ActiveSiteInfoPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSiteInfoPublisher.swift; sourceTree = ""; }; 7BB4BC692C5CD96200E06FC8 /* ActiveDomainPublisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveDomainPublisher.swift; sourceTree = ""; }; 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugUtilities.swift; sourceTree = ""; }; 7BCB90C12C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VPNControllerXPCClient+ConvenienceInitializers.swift"; sourceTree = ""; }; @@ -4151,6 +4172,10 @@ 7BDA36EA2B7E037200AD5388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VPNProxyExtension.entitlements; sourceTree = ""; }; 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = VPNProxyExtension.xcconfig; sourceTree = ""; }; + 7BDBAD102CBFF633000379B7 /* Logger+TipKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+TipKit.swift"; sourceTree = ""; }; + 7BDBAD112CBFF633000379B7 /* TipKitAppEventHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitAppEventHandling.swift; sourceTree = ""; }; + 7BDBAD122CBFF633000379B7 /* TipKitController+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TipKitController+ConvenienceInitializers.swift"; sourceTree = ""; }; + 7BDBAD132CBFF633000379B7 /* TipKitDebugOptionsUIActionHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitDebugOptionsUIActionHandling.swift; sourceTree = ""; }; 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugMenu.swift; sourceTree = ""; }; 7BEC182D2AD5D89C00D30536 /* SystemExtensionManager */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SystemExtensionManager; sourceTree = ""; }; 7BEC20402B0F505F00243D3E /* AddBookmarkPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkPopoverView.swift; sourceTree = ""; }; @@ -6297,7 +6322,7 @@ isa = PBXGroup; children = ( 7BB4BC692C5CD96200E06FC8 /* ActiveDomainPublisher.swift */, - 7BB4BC622C5BC13D00E06FC8 /* SiteTroubleshootingInfoPublisher.swift */, + 7BB4BC622C5BC13D00E06FC8 /* ActiveSiteInfoPublisher.swift */, 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */, BDE981DB2BBD110800645880 /* Assets */, 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */, @@ -7181,6 +7206,18 @@ path = VPNProxyExtension; sourceTree = ""; }; + 7BDBAD0F2CBFF60F000379B7 /* TipKit */ = { + isa = PBXGroup; + children = ( + 7BDBAD102CBFF633000379B7 /* Logger+TipKit.swift */, + 7BDBAD112CBFF633000379B7 /* TipKitAppEventHandling.swift */, + 7B22D86D2CCFD7B7006A76E1 /* TipKitController.swift */, + 7BDBAD122CBFF633000379B7 /* TipKitController+ConvenienceInitializers.swift */, + 7BDBAD132CBFF633000379B7 /* TipKitDebugOptionsUIActionHandling.swift */, + ); + path = TipKit; + sourceTree = ""; + }; 84537A072C99C1EF008723BC /* App */ = { isa = PBXGroup; children = ( @@ -7942,6 +7979,7 @@ AA86491B24D837DE001BABEE /* Tab */, AA86491124D8318F001BABEE /* TabBar */, AAE8B0FD258A416F00E81239 /* TabPreview */, + 7BDBAD0F2CBFF60F000379B7 /* TipKit */, B6040859274B8C5200680351 /* UnprotectedDomains */, 1D72D5902BFF361700AEDE36 /* Updates */, AACF6FD426BC35C200CF09F9 /* UserAgent */, @@ -11192,6 +11230,10 @@ EEC4A66E2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 3706FA83293F65D500E42796 /* LazyLoadable.swift in Sources */, 3199AF7C2C80734A003AEBDC /* DuckPlayerOnboardingLocationValidator.swift in Sources */, + 7BDBAD142CBFF633000379B7 /* TipKitAppEventHandling.swift in Sources */, + 7BDBAD152CBFF633000379B7 /* TipKitController+ConvenienceInitializers.swift in Sources */, + 7BDBAD162CBFF633000379B7 /* TipKitDebugOptionsUIActionHandling.swift in Sources */, + 7BDBAD172CBFF633000379B7 /* Logger+TipKit.swift in Sources */, 3706FA85293F65D500E42796 /* KeyedCodingExtension.swift in Sources */, 3706FA87293F65D500E42796 /* DownloadListStore.swift in Sources */, 37197EAB2942443D00394917 /* WebViewContainerView.swift in Sources */, @@ -11715,7 +11757,7 @@ 3706FBBC293F65D500E42796 /* NSViewExtension.swift in Sources */, 3706FBBE293F65D500E42796 /* DownloadListViewModel.swift in Sources */, 3706FBBF293F65D500E42796 /* BookmarkManagementDetailViewController.swift in Sources */, - 7BB4BC642C5BC13D00E06FC8 /* SiteTroubleshootingInfoPublisher.swift in Sources */, + 7BB4BC642C5BC13D00E06FC8 /* ActiveSiteInfoPublisher.swift in Sources */, 841BE93F2C6F236000E9C2B5 /* BookmarkDragDropManager.swift in Sources */, B6B4D1CB2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */, F188268E2BBF01C400D9AC4F /* PixelDataModel.xcdatamodeld in Sources */, @@ -11745,6 +11787,7 @@ 3758CBAB2CE63D540089FC2D /* NewTabPageWebViewModel.swift in Sources */, 9FA5A0A62BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */, 1DDC85002B835BC000670238 /* SearchPreferences.swift in Sources */, + 7B22D8712CCFD7B7006A76E1 /* TipKitController.swift in Sources */, 3706FBD0293F65D500E42796 /* WebKitVersionProvider.swift in Sources */, 3706FBD1293F65D500E42796 /* NSCoderExtensions.swift in Sources */, B6D6A5DD2982A4CE001F5F11 /* Tab+Navigation.swift in Sources */, @@ -12618,6 +12661,7 @@ 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, 7BA7CC592AD1203B0042E5CE /* UserText+NetworkProtection.swift in Sources */, 7BD7B0032C19D3830039D20A /* VPNIPCResources.swift in Sources */, + 7B22D86F2CCFD7B7006A76E1 /* TipKitController.swift in Sources */, F1DA51942BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */, 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, 7B2DDCFA2A93B25F0039D884 /* KeychainType+ClientDefault.swift in Sources */, @@ -12637,14 +12681,18 @@ F1C70D802BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 02FDA65B2C764C200024CD8B /* ConfigurationStore.swift in Sources */, + 7BDBAD202CBFF977000379B7 /* TipKitAppEventHandling.swift in Sources */, 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, + 7BDBAD1E2CBFF96A000379B7 /* Logger+TipKit.swift in Sources */, BDA764842BC49E3F00D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, + 7BDBAD1C2CBFF95F000379B7 /* TipKitController+ConvenienceInitializers.swift in Sources */, 7B60AFFA2C511B65008E32A3 /* VPNUIActionHandler.swift in Sources */, B65DA5EF2A77CC3A00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, BDA7647F2BC4998900D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 4BF0E5072AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, F1DA518A2BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, 7BA7CC442AD11E490042E5CE /* UserText.swift in Sources */, + 7BDBAD222CBFF97E000379B7 /* TipKitDebugOptionsUIActionHandling.swift in Sources */, 4BF0E5142AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, F1FDC93C2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, B65DA5F12A77D2BC00CBEE8D /* BundleExtension.swift in Sources */, @@ -12661,6 +12709,7 @@ 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */, 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, 7BD7B0042C19D3830039D20A /* VPNIPCResources.swift in Sources */, + 7B22D8702CCFD7B7006A76E1 /* TipKitController.swift in Sources */, F1DA51952BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, F1D0429E2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, @@ -12680,14 +12729,18 @@ B65DA5F02A77CC3C00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, BDA764852BC49E4000D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, 02FDA65D2C764CB30024CD8B /* ConfigurationStore.swift in Sources */, + 7BDBAD212CBFF977000379B7 /* TipKitAppEventHandling.swift in Sources */, 7BAF9E4D2A8A3CCB002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, + 7BDBAD1F2CBFF96A000379B7 /* Logger+TipKit.swift in Sources */, 7B60AFFB2C511C68008E32A3 /* VPNUIActionHandler.swift in Sources */, + 7BDBAD1D2CBFF95F000379B7 /* TipKitController+ConvenienceInitializers.swift in Sources */, 7BA7CC392AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */, BDA764802BC4998A00D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 7BA7CC552AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, F1DA518B2BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */, + 7BDBAD232CBFF97E000379B7 /* TipKitDebugOptionsUIActionHandling.swift in Sources */, F1FDC93D2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, 7BA7CC432AD11E480042E5CE /* UserText.swift in Sources */, 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, @@ -12973,6 +13026,10 @@ 1DDF076428F815AD00EDFBE3 /* BWManager.swift in Sources */, 9833912F27AAA3CE00DAF119 /* AppTrackerDataSetProvider.swift in Sources */, B65211252B29A42C00B30633 /* BookmarkStoreMock.swift in Sources */, + 7BDBAD182CBFF633000379B7 /* TipKitAppEventHandling.swift in Sources */, + 7BDBAD192CBFF633000379B7 /* TipKitController+ConvenienceInitializers.swift in Sources */, + 7BDBAD1A2CBFF633000379B7 /* TipKitDebugOptionsUIActionHandling.swift in Sources */, + 7BDBAD1B2CBFF633000379B7 /* Logger+TipKit.swift in Sources */, 4BA1A6B3258B080A00F6F690 /* EncryptionKeyGeneration.swift in Sources */, 37B11B3928095E6600CBB621 /* TabLazyLoader.swift in Sources */, 4B9DB03B2A983B24000927DB /* InvitedToWaitlistView.swift in Sources */, @@ -13089,7 +13146,7 @@ AA512D1424D99D9800230283 /* FaviconManager.swift in Sources */, 3148723B2CC64A5F00EEF89B /* AIChatToolBarPopUpOnboardingView.swift in Sources */, 7BB108592A43375D000AB95F /* PFMoveApplication.m in Sources */, - 7BB4BC632C5BC13D00E06FC8 /* SiteTroubleshootingInfoPublisher.swift in Sources */, + 7BB4BC632C5BC13D00E06FC8 /* ActiveSiteInfoPublisher.swift in Sources */, 4B0AACAC28BC63ED001038AC /* ChromiumFaviconsReader.swift in Sources */, AABEE6AB24ACA0F90043105B /* SuggestionTableRowView.swift in Sources */, 37CD54CB27F2FDD100F1F7B9 /* DownloadsPreferences.swift in Sources */, @@ -13288,6 +13345,7 @@ AAC5E4C725D6A6E8007F5990 /* AddBookmarkPopover.swift in Sources */, B6F7127E29F6779000594A45 /* QRSharingService.swift in Sources */, 3768D83B2C24C0A8004120AE /* RemoteMessageViewModel.swift in Sources */, + 7B22D86E2CCFD7B7006A76E1 /* TipKitController.swift in Sources */, B68C2FB227706E6A00BF2C7D /* ProcessExtension.swift in Sources */, CD2AB5CA2C8225E70019EB49 /* URLTokenValidator.swift in Sources */, B6106BA726A7BECC0013B453 /* PermissionAuthorizationQuery.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index e52d0e4ce7..ca53eea2ab 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -468,6 +468,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { DataBrokerProtectionAppEvents(featureGatekeeper: pirGatekeeper).applicationDidFinishLaunching() + TipKitAppEventHandler(featureFlagger: featureFlagger).appDidFinishLaunching() + setUpAutoClearHandler() setUpAutofillPixelReporter() diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index e27993350e..f0696f7c89 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -728,6 +728,11 @@ final class MainMenu: NSMenu { openSubscriptionTab: { WindowControllersManager.shared.showTab(with: .subscription($0)) }, subscriptionManager: Application.appDelegate.subscriptionManager) + NSMenuItem(title: "TipKit") { + NSMenuItem(title: "Reset", action: #selector(MainViewController.resetTipKit)) + NSMenuItem(title: "⚠️ App restart required.", action: nil, target: nil) + } + NSMenuItem(title: "Logging").submenu(setupLoggingMenu()) NSMenuItem(title: "AI Chat").submenu(AIChatDebugMenu()) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 7249891f0e..dda445c349 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -901,6 +901,10 @@ extension MainViewController { SyncPromoManager().resetPromos() } + @objc func resetTipKit(_ sender: Any?) { + TipKitDebugOptionsUIActionHandler().resetTipKitTapped() + } + @objc func internalUserState(_ sender: Any?) { guard let internalUserDecider = NSApp.delegateTyped.internalUserDecider as? DefaultInternalUserDecider else { return } let state = internalUserDecider.isInternalUser diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/ActiveDomainPublisher.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/ActiveDomainPublisher.swift index 4a77857ae6..24648e7ef9 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/ActiveDomainPublisher.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/ActiveDomainPublisher.swift @@ -43,7 +43,13 @@ final class ActiveDomainPublisher { } } + @MainActor init(windowControllersManager: WindowControllersManager) { + + if let tabContent = windowControllersManager.lastKeyMainWindowController?.activeTab?.content { + activeDomain = Self.domain(from: tabContent) + } + self.windowControllersManager = windowControllersManager Task { @MainActor in @@ -73,7 +79,7 @@ final class ActiveDomainPublisher { @MainActor private func subscribeToActiveTabContentChanges() { activeTabContentCancellable = activeTab?.$content - .map(domain(from:)) + .map(Self.domain(from:)) .removeDuplicates() .assign(to: \.activeDomain, onWeaklyHeld: self) } @@ -88,7 +94,7 @@ final class ActiveDomainPublisher { } } - private func domain(from tabContent: Tab.TabContent) -> String? { + private static func domain(from tabContent: Tab.TabContent) -> String? { if case .url(let url, _, _) = tabContent { return url.host diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/SiteTroubleshootingInfoPublisher.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/ActiveSiteInfoPublisher.swift similarity index 72% rename from DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/SiteTroubleshootingInfoPublisher.swift rename to DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/ActiveSiteInfoPublisher.swift index 3facdea39e..ad5375594d 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/SiteTroubleshootingInfoPublisher.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/ActiveSiteInfoPublisher.swift @@ -1,5 +1,5 @@ // -// SiteTroubleshootingInfoPublisher.swift +// ActiveSiteInfoPublisher.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -22,15 +22,15 @@ import NetworkProtectionProxy import NetworkProtectionUI @MainActor -final class SiteTroubleshootingInfoPublisher { +final class ActiveSiteInfoPublisher { private var activeDomain: String? { didSet { - refreshSiteTroubleshootingInfo() + refreshActiveSiteInfo() } } - private let subject: CurrentValueSubject + private let subject: CurrentValueSubject private let activeDomainPublisher: AnyPublisher private let proxySettings: TransparentProxySettings @@ -39,7 +39,7 @@ final class SiteTroubleshootingInfoPublisher { init(activeDomainPublisher: AnyPublisher, proxySettings: TransparentProxySettings) { - subject = CurrentValueSubject(nil) + subject = CurrentValueSubject(nil) self.activeDomainPublisher = activeDomainPublisher self.proxySettings = proxySettings @@ -59,7 +59,7 @@ final class SiteTroubleshootingInfoPublisher { switch change { case .excludedDomains: - refreshSiteTroubleshootingInfo() + refreshActiveSiteInfo() default: break } @@ -68,15 +68,15 @@ final class SiteTroubleshootingInfoPublisher { // MARK: - Refreshing - func refreshSiteTroubleshootingInfo() { - if activeSiteTroubleshootingInfo != subject.value { - subject.send(activeSiteTroubleshootingInfo) + func refreshActiveSiteInfo() { + if activeActiveSiteInfo != subject.value { + subject.send(activeActiveSiteInfo) } } // MARK: - Active Site Troubleshooting Info - var activeSiteTroubleshootingInfo: SiteTroubleshootingInfo? { + var activeActiveSiteInfo: ActiveSiteInfo? { guard let activeDomain else { return nil } @@ -84,13 +84,13 @@ final class SiteTroubleshootingInfoPublisher { return site(forDomain: activeDomain.droppingWwwPrefix()) } - private func site(forDomain domain: String) -> SiteTroubleshootingInfo? { + private func site(forDomain domain: String) -> ActiveSiteInfo? { let icon: NSImage? - let currentSite: NetworkProtectionUI.SiteTroubleshootingInfo? + let currentSite: NetworkProtectionUI.ActiveSiteInfo? icon = FaviconManager.shared.getCachedFavicon(forDomainOrAnySubdomain: domain, sizeCategory: .small)?.image let proxySettings = TransparentProxySettings(defaults: .netP) - currentSite = NetworkProtectionUI.SiteTroubleshootingInfo( + currentSite = NetworkProtectionUI.ActiveSiteInfo( icon: icon, domain: domain, excluded: proxySettings.isExcluding(domain: domain)) @@ -99,12 +99,12 @@ final class SiteTroubleshootingInfoPublisher { } } -extension SiteTroubleshootingInfoPublisher: Publisher { - typealias Output = SiteTroubleshootingInfo? +extension ActiveSiteInfoPublisher: Publisher { + typealias Output = ActiveSiteInfo? typealias Failure = Never nonisolated - func receive(subscriber: S) where S: Subscriber, Never == S.Failure, NetworkProtectionUI.SiteTroubleshootingInfo? == S.Input { + func receive(subscriber: S) where S: Subscriber, Never == S.Failure, NetworkProtectionUI.ActiveSiteInfo? == S.Input { subject.receive(subscriber: subscriber) } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 14966d2a3c..3fd21aa827 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -24,11 +24,14 @@ import Foundation import LoginItems import NetworkProtection import NetworkProtectionIPC +import NetworkProtectionProxy import NetworkProtectionUI +import os.log import Subscription -import VPNAppLauncher import SwiftUI -import NetworkProtectionProxy +import VPNAppLauncher +import BrowserServicesKit +import FeatureFlags protocol NetworkProtectionIPCClient { var ipcStatusObserver: ConnectionStatusObserver { get } @@ -55,8 +58,8 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { let vpnUninstaller: VPNUninstalling @Published - private var siteInfo: SiteTroubleshootingInfo? - private let siteTroubleshootingInfoPublisher: SiteTroubleshootingInfoPublisher + private var siteInfo: ActiveSiteInfo? + private let activeSitePublisher: ActiveSiteInfoPublisher private var cancellables = Set() init(ipcClient: VPNControllerXPCClient, @@ -67,7 +70,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { let activeDomainPublisher = ActiveDomainPublisher(windowControllersManager: .shared) - siteTroubleshootingInfoPublisher = SiteTroubleshootingInfoPublisher( + activeSitePublisher = ActiveSiteInfoPublisher( activeDomainPublisher: activeDomainPublisher.eraseToAnyPublisher(), proxySettings: TransparentProxySettings(defaults: .netP)) @@ -75,7 +78,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { } private func subscribeToCurrentSitePublisher() { - siteTroubleshootingInfoPublisher + activeSitePublisher .assign(to: \.siteInfo, onWeaklyHeld: self) .store(in: &cancellables) } @@ -87,9 +90,10 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) -> NSPopover { /// Since the favicon doesn't have a publisher we force refreshing here - siteTroubleshootingInfoPublisher.refreshSiteTroubleshootingInfo() + activeSitePublisher.refreshActiveSiteInfo() let popover: NSPopover = { + let vpnSettings = VPNSettings(defaults: .netP) let controller = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) let statusReporter = DefaultNetworkProtectionStatusReporter( @@ -103,15 +107,22 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { ) let onboardingStatusPublisher = UserDefaults.netP.networkProtectionOnboardingStatusPublisher - _ = VPNSettings(defaults: .netP) let appLauncher = AppLauncher(appBundleURL: Bundle.main.bundleURL) let vpnURLEventHandler = VPNURLEventHandler() let proxySettings = TransparentProxySettings(defaults: .netP) let uiActionHandler = VPNUIActionHandler(vpnURLEventHandler: vpnURLEventHandler, proxySettings: proxySettings) + let connectionStatusPublisher = CurrentValuePublisher( + initialValue: statusReporter.statusObserver.recentValue, + publisher: statusReporter.statusObserver.publisher) + + let activeSitePublisher = CurrentValuePublisher( + initialValue: siteInfo, + publisher: $siteInfo.eraseToAnyPublisher()) + let siteTroubleshootingViewModel = SiteTroubleshootingView.Model( - connectionStatusPublisher: statusReporter.statusObserver.publisher, - siteTroubleshootingInfoPublisher: $siteInfo.eraseToAnyPublisher(), + connectionStatusPublisher: connectionStatusPublisher, + activeSitePublisher: activeSitePublisher, uiActionHandler: uiActionHandler) let statusViewModel = NetworkProtectionStatusView.Model(controller: controller, @@ -157,10 +168,36 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { _ = try? await self?.vpnUninstaller.uninstall(removeSystemExtension: true) }) + let featureFlagger = NSApp.delegateTyped.featureFlagger + let tipsFeatureFlagInitialValue = featureFlagger.isFeatureOn(.networkProtectionUserTips) + let tipsFeatureFlagPublisher: CurrentValuePublisher + + if let overridesHandler = featureFlagger.localOverrides?.actionHandler as? FeatureFlagOverridesPublishingHandler { + + let featureFlagPublisher = overridesHandler.flagDidChangePublisher + .filter { $0.0 == .networkProtectionUserTips } + + tipsFeatureFlagPublisher = CurrentValuePublisher( + initialValue: tipsFeatureFlagInitialValue, + publisher: Just(tipsFeatureFlagInitialValue).eraseToAnyPublisher()) + } else { + tipsFeatureFlagPublisher = CurrentValuePublisher( + initialValue: tipsFeatureFlagInitialValue, + publisher: Just(tipsFeatureFlagInitialValue).eraseToAnyPublisher()) + } + + let tipsModel = VPNTipsModel(featureFlagPublisher: tipsFeatureFlagPublisher, + statusObserver: statusReporter.statusObserver, + activeSitePublisher: activeSitePublisher, + forMenuApp: false, + vpnSettings: vpnSettings, + logger: Logger(subsystem: "DuckDuckGo", category: "TipKit")) + let popover = NetworkProtectionPopover( statusViewModel: statusViewModel, statusReporter: statusReporter, siteTroubleshootingViewModel: siteTroubleshootingViewModel, + tipsModel: tipsModel, debugInformationViewModel: DebugInformationViewModel(showDebugInformation: false)) popover.delegate = delegate diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index fd841fcedf..76dbc16634 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -32,12 +32,20 @@ final class VPNPreferencesModel: ObservableObject { @Published var connectOnLogin: Bool { didSet { + guard settings.connectOnLogin != connectOnLogin else { + return + } + settings.connectOnLogin = connectOnLogin } } @Published var excludeLocalNetworks: Bool { didSet { + guard settings.excludeLocalNetworks != excludeLocalNetworks else { + return + } + settings.excludeLocalNetworks = excludeLocalNetworks Task { @@ -49,8 +57,6 @@ final class VPNPreferencesModel: ObservableObject { } } - @Published var secureDNS: Bool = true - @Published var showInMenuBar: Bool { didSet { settings.showInMenuBar = showInMenuBar @@ -117,6 +123,8 @@ final class VPNPreferencesModel: ObservableObject { locationItem = VPNLocationPreferenceItemModel(selectedLocation: settings.selectedLocation) subscribeToOnboardingStatusChanges(defaults: defaults) + subscribeToConnectOnLoginSettingChanges() + subscribeToExcludeLocalNetworksSettingChanges() subscribeToShowInMenuBarSettingChanges() subscribeToShowInBrowserToolbarSettingsChanges() subscribeToLocationSettingChanges() @@ -129,6 +137,18 @@ final class VPNPreferencesModel: ObservableObject { .store(in: &cancellables) } + func subscribeToConnectOnLoginSettingChanges() { + settings.connectOnLoginPublisher + .assign(to: \.connectOnLogin, onWeaklyHeld: self) + .store(in: &cancellables) + } + + func subscribeToExcludeLocalNetworksSettingChanges() { + settings.excludeLocalNetworksPublisher + .assign(to: \.excludeLocalNetworks, onWeaklyHeld: self) + .store(in: &cancellables) + } + func subscribeToShowInMenuBarSettingChanges() { settings.showInMenuBarPublisher .removeDuplicates() diff --git a/DuckDuckGo/TipKit/Logger+TipKit.swift b/DuckDuckGo/TipKit/Logger+TipKit.swift new file mode 100644 index 0000000000..26941cfc6f --- /dev/null +++ b/DuckDuckGo/TipKit/Logger+TipKit.swift @@ -0,0 +1,27 @@ +// +// Logger+TipKit.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log + +extension Logger { + + static var tipKit: Logger = { + Logger(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "TipKit") + }() +} diff --git a/DuckDuckGo/TipKit/TipKitAppEventHandling.swift b/DuckDuckGo/TipKit/TipKitAppEventHandling.swift new file mode 100644 index 0000000000..da802fd49f --- /dev/null +++ b/DuckDuckGo/TipKit/TipKitAppEventHandling.swift @@ -0,0 +1,58 @@ +// +// TipKitAppEventHandling.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import BrowserServicesKit +import os.log +import TipKit + +protocol TipKitAppEventHandling { + func appDidFinishLaunching() +} + +struct TipKitAppEventHandler: TipKitAppEventHandling { + + private let controller: TipKitController + private let featureFlagger: FeatureFlagger + private let logger: Logger + + init(controller: TipKitController = .make(), + featureFlagger: FeatureFlagger, + logger: Logger = .tipKit) { + + self.controller = controller + self.featureFlagger = featureFlagger + self.logger = logger + } + + func appDidFinishLaunching() { + guard featureFlagger.isFeatureOn(.networkProtectionUserTips) else { + logger.log("TipKit disabled by remote feature flag.") + return + } + + if #available(macOS 14.0, *) { + controller.configureTipKit([ + .displayFrequency(.immediate), + .datastoreLocation(.applicationDefault) + ]) + } else { + logger.log("TipKit initialization skipped: macOS 14.0 or later is required.") + } + } +} diff --git a/DuckDuckGo/TipKit/TipKitController+ConvenienceInitializers.swift b/DuckDuckGo/TipKit/TipKitController+ConvenienceInitializers.swift new file mode 100644 index 0000000000..ce1de51c30 --- /dev/null +++ b/DuckDuckGo/TipKit/TipKitController+ConvenienceInitializers.swift @@ -0,0 +1,29 @@ +// +// TipKitController+ConvenienceInitializers.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os + +extension TipKitController { + + static func make(logger: Logger = .tipKit, + userDefaults: UserDefaults = .appConfiguration) -> Self { + + self.init(logger: logger, userDefaults: userDefaults) + } +} diff --git a/DuckDuckGo/TipKit/TipKitController.swift b/DuckDuckGo/TipKit/TipKitController.swift new file mode 100644 index 0000000000..8b8ff507e3 --- /dev/null +++ b/DuckDuckGo/TipKit/TipKitController.swift @@ -0,0 +1,92 @@ +// +// TipKitController.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log +import TipKit + +protocol TipKitControlling { + @available(iOS 17.0, *) + func configureTipKit() + + @available(iOS 17.0, *) + func resetTipKitOnNextAppLaunch() +} + +final class TipKitController { + + private let logger: Logger + private let userDefaults: UserDefaults + + private var resetTipKitOnNextLaunch: Bool { + get { + userDefaults.bool(forKey: "resetTipKitOnNextLaunch") + } + + set { + userDefaults.set(newValue, forKey: "resetTipKitOnNextLaunch") + } + } + + init(logger: Logger, + userDefaults: UserDefaults) { + + self.logger = logger + self.userDefaults = userDefaults + } + + @available(iOS 17.0, macOS 14.0, *) + func configureTipKit(_ configuration: [Tips.ConfigurationOption] = []) { + do { + if resetTipKitOnNextLaunch { + resetTipKit() + resetTipKitOnNextLaunch = false + } + + try Tips.configure(configuration) + + logger.debug("TipKit initialized") + } catch { + logger.error("Failed to initialize TipKit: \(error)") + } + } + + @available(iOS 17.0, macOS 14.0, *) + private func resetTipKit() { + do { + try Tips.resetDatastore() + + logger.debug("TipKit reset") + } catch { + logger.debug("Failed to reset TipKit: \(error)") + } + } + + /// Resets TipKit + /// + /// One thing that's not documented as of 2024-10-09 is that resetting TipKit must happen before it's configured. + /// When trying to reset it after it's configured we get `TipKit.TipKitError(value: TipKit.TipKitError.Value.tipsDatastoreAlreadyConfigured)`. + /// In order to make things work for us we set a user defaults value that ensures TipKit will be reset on next + /// app launch instead of directly trying to reset it here. + /// + @available(iOS 17.0, *) + func resetTipKitOnNextAppLaunch() { + resetTipKitOnNextLaunch = true + logger.debug("TipKit will reset on next app launch") + } +} diff --git a/DuckDuckGo/TipKit/TipKitDebugOptionsUIActionHandling.swift b/DuckDuckGo/TipKit/TipKitDebugOptionsUIActionHandling.swift new file mode 100644 index 0000000000..2ff60e2e4c --- /dev/null +++ b/DuckDuckGo/TipKit/TipKitDebugOptionsUIActionHandling.swift @@ -0,0 +1,46 @@ +// +// TipKitDebugOptionsUIActionHandling.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log + +protocol TipKitDebugOptionsUIActionHandling { + /// Resets TipKit + func resetTipKitTapped() +} + +struct TipKitDebugOptionsUIActionHandler: TipKitDebugOptionsUIActionHandling { + + private let controller: TipKitController + private let logger: Logger + + init(controller: TipKitController = .make(), + logger: Logger = .tipKit) { + + self.controller = controller + self.logger = logger + } + + func resetTipKitTapped() { + if #available(macOS 14.0, *) { + controller.resetTipKitOnNextAppLaunch() + } else { + logger.log("TipKit initialization skipped: macOS 14.0 or later is required.") + } + } +} diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 6f8115af19..2748926f11 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -375,6 +375,10 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { // Load cached config (if any) privacyConfigurationManager.reload(etag: configurationStore.loadEtag(for: .privacyConfiguration), data: configurationStore.loadData(for: .privacyConfiguration)) + // It's important for this to be set-up after the privacy configuration is loaded + // as it relies on it for the remote feature flag. + TipKitAppEventHandler(featureFlagger: featureFlagger).appDidFinishLaunching() + setupMenuVisibility() Task { @MainActor in diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Combine/CurrentValuePublisher.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Combine/CurrentValuePublisher.swift new file mode 100644 index 0000000000..557147b340 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Combine/CurrentValuePublisher.swift @@ -0,0 +1,48 @@ +// +// CurrentValuePublisher.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation + +public final class CurrentValuePublisher { + + private(set) public var value: Output + private let wrappedPublisher: AnyPublisher + private var cancellable: AnyCancellable? + + public init(initialValue: Output, publisher: AnyPublisher) { + value = initialValue + wrappedPublisher = publisher + + subscribeToPublisherUpdates() + } + + private func subscribeToPublisherUpdates() { + cancellable = wrappedPublisher + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in }) { [weak self] value in + self?.value = value + } + } +} + +extension CurrentValuePublisher: Publisher { + public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { + wrappedPublisher.receive(subscriber: subscriber) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/LottieView+withIntro.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/LottieView+withIntro.swift index bfa0e14367..a4cfe5d07c 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/LottieView+withIntro.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/LottieView+withIntro.swift @@ -28,14 +28,14 @@ extension LottieView where Placeholder: View { let loopEndFrame: AnimationFrameTime } - public func playing(withIntro timing: LoopWithIntroTiming, isAnimating: Binding = .constant(true)) -> Lottie.LottieView { + public func playing(withIntro timing: LoopWithIntroTiming, isAnimating: Bool = true) -> Lottie.LottieView { configure { uiView in - if uiView.isAnimationPlaying, !isAnimating.wrappedValue { + if uiView.isAnimationPlaying, !isAnimating { uiView.stop() return } - guard isAnimating.wrappedValue, !uiView.isAnimationPlaying else { return } + guard isAnimating, !uiView.isAnimationPlaying else { return } if uiView.loopMode == .playOnce, uiView.currentProgress == 1 { return } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift index 34ac66d9ae..d5730c40ce 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift @@ -19,9 +19,11 @@ import AppKit import Foundation import Combine -import SwiftUI -import NetworkProtection +import Common import LoginItems +import NetworkProtection +import os.log +import SwiftUI /// Abstraction of the the VPN status bar menu with a simple interface. /// @@ -134,11 +136,31 @@ public final class StatusBarMenu: NSObject { return } + let connectionStatusPublisher = CurrentValuePublisher( + initialValue: .disconnected, + publisher: Just(NetworkProtection.ConnectionStatus.disconnected).eraseToAnyPublisher()) + + let activeSitePublisher = CurrentValuePublisher( + initialValue: ActiveSiteInfo?(nil), + publisher: Just(nil).eraseToAnyPublisher()) + let siteTroubleshootingViewModel = SiteTroubleshootingView.Model( - connectionStatusPublisher: Just(NetworkProtection.ConnectionStatus.disconnected).eraseToAnyPublisher(), - siteTroubleshootingInfoPublisher: Just(SiteTroubleshootingInfo?(nil)).eraseToAnyPublisher(), + connectionStatusPublisher: connectionStatusPublisher, + activeSitePublisher: activeSitePublisher, uiActionHandler: uiActionHandler) + // We don't use tips in the status menu app. + let tipsFeatureFlagPublisher = CurrentValuePublisher( + initialValue: false, + publisher: Just(false).eraseToAnyPublisher()) + + let tipsModel = VPNTipsModel(featureFlagPublisher: tipsFeatureFlagPublisher, + statusObserver: statusReporter.statusObserver, + activeSitePublisher: activeSitePublisher, + forMenuApp: true, + vpnSettings: VPNSettings(defaults: userDefaults), + logger: Logger(subsystem: "DuckDuckGo", category: "TipKit")) + let debugInformationViewModel = DebugInformationViewModel(showDebugInformation: isOptionKeyPressed) let statusViewModel = NetworkProtectionStatusView.Model( @@ -157,6 +179,7 @@ public final class StatusBarMenu: NSObject { statusViewModel: statusViewModel, statusReporter: statusReporter, siteTroubleshootingViewModel: siteTroubleshootingViewModel, + tipsModel: tipsModel, debugInformationViewModel: debugInformationViewModel) popover?.behavior = .transient diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift index e780a0cb29..dd6b7b3e2f 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift @@ -32,16 +32,19 @@ public final class NetworkProtectionPopover: NSPopover { private let debugInformationViewModel: DebugInformationViewModel private let siteTroubleshootingViewModel: SiteTroubleshootingView.Model private let statusViewModel: NetworkProtectionStatusView.Model + private let tipsModel: VPNTipsModel private var appLifecycleCancellables = Set() public required init(statusViewModel: NetworkProtectionStatusView.Model, statusReporter: NetworkProtectionStatusReporter, siteTroubleshootingViewModel: SiteTroubleshootingView.Model, + tipsModel: VPNTipsModel, debugInformationViewModel: DebugInformationViewModel) { self.statusReporter = statusReporter self.debugInformationViewModel = debugInformationViewModel self.siteTroubleshootingViewModel = siteTroubleshootingViewModel + self.tipsModel = tipsModel self.statusViewModel = statusViewModel super.init() @@ -62,6 +65,7 @@ public final class NetworkProtectionPopover: NSPopover { .environmentObject(debugInformationViewModel) .environmentObject(siteTroubleshootingViewModel) .environmentObject(statusViewModel) + .environmentObject(tipsModel) .environment(\.dismiss, { [weak self] in self?.close() }).fixedSize() diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/TipBackground.colorset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/TipBackground.colorset/Contents.json new file mode 100644 index 0000000000..48eecc1685 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/TipBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.400", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.200", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/autoconnectTip.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/autoconnectTip.imageset/Contents.json new file mode 100644 index 0000000000..61e3f585d4 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/autoconnectTip.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Power-Reconnect-32.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/autoconnectTip.imageset/Power-Reconnect-32.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/autoconnectTip.imageset/Power-Reconnect-32.pdf new file mode 100644 index 0000000000..667ac9a998 Binary files /dev/null and b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/autoconnectTip.imageset/Power-Reconnect-32.pdf differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/domainExclusionsTip.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/domainExclusionsTip.imageset/Contents.json new file mode 100644 index 0000000000..0640c66ec7 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/domainExclusionsTip.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Site-Broken-32.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/domainExclusionsTip.imageset/Site-Broken-32.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/domainExclusionsTip.imageset/Site-Broken-32.pdf new file mode 100644 index 0000000000..05129144c9 Binary files /dev/null and b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/domainExclusionsTip.imageset/Site-Broken-32.pdf differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/geolocationTip.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/geolocationTip.imageset/Contents.json new file mode 100644 index 0000000000..cb612d2e7d --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/geolocationTip.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Location-32.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/geolocationTip.imageset/Location-32.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/geolocationTip.imageset/Location-32.pdf new file mode 100644 index 0000000000..25b89dda77 Binary files /dev/null and b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/geolocationTip.imageset/Location-32.pdf differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingInfo.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/ActiveSiteInfo/ActiveSiteInfo.swift similarity index 88% rename from LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingInfo.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/ActiveSiteInfo/ActiveSiteInfo.swift index 33df4b11bb..3a20333bc7 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingInfo.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/ActiveSiteInfo/ActiveSiteInfo.swift @@ -1,5 +1,5 @@ // -// SiteTroubleshootingInfo.swift +// ActiveSiteInfo.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -19,7 +19,7 @@ import AppKit import Foundation -public struct SiteTroubleshootingInfo { +public struct ActiveSiteInfo { public let icon: NSImage? public let domain: String public let excluded: Bool @@ -31,6 +31,4 @@ public struct SiteTroubleshootingInfo { } } -extension SiteTroubleshootingInfo: Equatable { - -} +extension ActiveSiteInfo: Equatable {} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingView.swift index ccf2eae271..b8f5e0d929 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingView.swift @@ -27,6 +27,9 @@ public struct SiteTroubleshootingView: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var model: Model + @EnvironmentObject + private var tipsModel: VPNTipsModel + // MARK: - View Contents public var body: some View { @@ -38,7 +41,7 @@ public struct SiteTroubleshootingView: View { } @ViewBuilder - private func siteTroubleshootingView(_ siteInfo: SiteTroubleshootingInfo) -> some View { + private func siteTroubleshootingView(_ siteInfo: ActiveSiteInfo) -> some View { Divider() .padding(EdgeInsets(top: 5, leading: 9, bottom: 5, trailing: 9)) @@ -51,6 +54,10 @@ public struct SiteTroubleshootingView: View { Toggle(isOn: Binding(get: { siteInfo.excluded }, set: { value in + if #available(macOS 14.0, *) { + tipsModel.handleSiteExcluded() + } + model.setExclusion(value, forDomain: siteInfo.domain) })) { HStack(spacing: 5) { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingViewModel.swift index ceb9c6336a..3980694a2d 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingViewModel.swift @@ -31,9 +31,9 @@ extension SiteTroubleshootingView { private(set) var connectionStatus: ConnectionStatus = .disconnected @Published - private var internalSiteInfo: SiteTroubleshootingInfo? + private var internalSiteInfo: ActiveSiteInfo? - var siteInfo: SiteTroubleshootingInfo? { + var siteInfo: ActiveSiteInfo? { guard case .connected = connectionStatus else { return nil } @@ -45,19 +45,21 @@ extension SiteTroubleshootingView { private let pixelKit: PixelFiring? private var cancellables = Set() - public init(connectionStatusPublisher: AnyPublisher, - siteTroubleshootingInfoPublisher: AnyPublisher, + public init(connectionStatusPublisher: CurrentValuePublisher, + activeSitePublisher: CurrentValuePublisher, uiActionHandler: VPNUIActionHandling, pixelKit: PixelFiring? = PixelKit.shared) { + connectionStatus = connectionStatusPublisher.value self.uiActionHandler = uiActionHandler self.pixelKit = pixelKit + internalSiteInfo = activeSitePublisher.value subscribeToConnectionStatusChanges(connectionStatusPublisher) - subscribeToSiteTroubleshootingInfoChanges(siteTroubleshootingInfoPublisher) + subscribeToActiveSiteInfoChanges(activeSitePublisher) } - private func subscribeToConnectionStatusChanges(_ publisher: AnyPublisher) { + private func subscribeToConnectionStatusChanges(_ publisher: CurrentValuePublisher) { publisher .receive(on: DispatchQueue.main) @@ -65,7 +67,7 @@ extension SiteTroubleshootingView { .store(in: &cancellables) } - private func subscribeToSiteTroubleshootingInfoChanges(_ publisher: AnyPublisher) { + private func subscribeToActiveSiteInfoChanges(_ publisher: CurrentValuePublisher) { publisher .receive(on: DispatchQueue.main) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNAutoconnectTip.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNAutoconnectTip.swift new file mode 100644 index 0000000000..676fe645aa --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNAutoconnectTip.swift @@ -0,0 +1,74 @@ +// +// VPNAutoconnectTip.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import TipKit + +/// A tip to suggest to the user to use the autoconnect option for the VPN. +/// +struct VPNAutoconnectTip {} + +@available(macOS 14.0, *) +extension VPNAutoconnectTip: Tip { + + enum ActionIdentifiers: String { + case enable = "com.duckduckgo.vpn.tip.autoconnect.action.enable" + } + + @Parameter(.transient) + static var vpnEnabled: Bool = false + + /// This condition tries to verify that this tip is distanced from the previous tip.. + /// + /// The conditions that will trigger this are: + /// - The status view was opened when previous tip's status is invalidated. + /// - The VPN is enabled when previous tip's status is invalidated. + /// + @Parameter + static var isDistancedFromPreviousTip: Bool = false + + var id: String { + "com.duckduckgo.vpn.tip.autoconnect" + } + + var title: Text { + Text("Connect Automatically") + } + + var message: Text? { + Text("The VPN can connect on its own when you log in to your computer.") + } + + var image: Image? { + Image(.autoconnectTip) + } + + var actions: [Action] { + [Action(id: ActionIdentifiers.enable.rawValue) { + Text("Enable") + }] + } + + var rules: [Rule] { + #Rule(Self.$vpnEnabled) { + $0 == true + } + #Rule(Self.$isDistancedFromPreviousTip) { + $0 + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNDomainExclusionsTip.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNDomainExclusionsTip.swift new file mode 100644 index 0000000000..f166e82375 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNDomainExclusionsTip.swift @@ -0,0 +1,70 @@ +// +// VPNDomainExclusionsTip.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import TipKit + +/// A tip to suggest using domain exclusions when a site doesn't work. +/// +struct VPNDomainExclusionsTip {} + +@available(macOS 14.0, *) +extension VPNDomainExclusionsTip: Tip { + + @Parameter(.transient) + static var vpnEnabled: Bool = false + + @Parameter(.transient) + static var hasActiveSite: Bool = false + + /// This condition tries to verify that this tip is distanced from the previous tip and doesn't show right after. + /// + /// The conditions that will trigger this are: + /// - The status view was opened when previous tip's status is invalidated. + /// - The VPN is enabled when previous tip's status is invalidated. + /// + @Parameter + static var isDistancedFromPreviousTip: Bool = false + + var id: String { + "com.duckduckgo.vpn.tip.domainExclusions" + } + + var title: Text { + Text("Website not working?") + } + + var message: Text? { + Text("Exclude websites that block VPN traffic so you can use them without turning the VPN off.") + } + + var image: Image? { + Image(.domainExclusionsTip) + } + + var rules: [Self.Rule] { + #Rule(Self.$hasActiveSite) { + $0 + } + #Rule(Self.$vpnEnabled) { + $0 + } + #Rule(Self.$isDistancedFromPreviousTip) { + $0 + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNGeoswitchingTip.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNGeoswitchingTip.swift new file mode 100644 index 0000000000..3b5a9e5173 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNGeoswitchingTip.swift @@ -0,0 +1,56 @@ +// +// VPNGeoswitchingTip.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import TipKit + +/// A tip to suggest to the user to change their location using geo-switching +/// +struct VPNGeoswitchingTip {} + +@available(macOS 14.0, *) +extension VPNGeoswitchingTip: Tip { + + /// Where the VPN was ever enabled. + /// + /// Once set this is never unset. The tip doesn't need to be hidden when the user is disconnected. + /// + @Parameter + static var vpnEnabledOnce: Bool = false + + var id: String { + "com.duckduckgo.vpn.tip.geoswitching" + } + + var title: Text { + Text("Change Your Location") + } + + var message: Text? { + Text("Connect to any of our servers worldwide to customize the VPN location.") + } + + var image: Image? { + Image(.geolocationTip) + } + + var rules: [Rule] { + #Rule(Self.$vpnEnabledOnce) { + $0 + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNTipsModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNTipsModel.swift new file mode 100644 index 0000000000..fd7a2518dc --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNTipsModel.swift @@ -0,0 +1,274 @@ +// +// VPNTipsModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Combine +import Common +import NetworkProtection +import os.log +import TipKit +import PixelKit +import VPNPixels + +@MainActor +public final class VPNTipsModel: ObservableObject { + + static let imageSize = CGSize(width: 32, height: 32) + + @Published + private(set) var activeSiteInfo: ActiveSiteInfo? { + didSet { + guard #available(macOS 14.0, *) else { + return + } + + handleActiveSiteInfoChanged(newValue: activeSiteInfo) + } + } + + @Published + private(set) var connectionStatus: ConnectionStatus { + didSet { + guard #available(macOS 14.0, *) else { + return + } + + handleConnectionStatusChanged(oldValue: oldValue, newValue: connectionStatus) + } + } + + @Published + private var featureFlag: Bool + + private let isMenuApp: Bool + private let vpnSettings: VPNSettings + private let logger: Logger + private var cancellables = Set() + + public init(featureFlagPublisher: CurrentValuePublisher, + statusObserver: ConnectionStatusObserver, + activeSitePublisher: CurrentValuePublisher, + forMenuApp isMenuApp: Bool, + vpnSettings: VPNSettings, + logger: Logger) { + + self.activeSiteInfo = activeSitePublisher.value + self.connectionStatus = statusObserver.recentValue + self.featureFlag = featureFlagPublisher.value + self.isMenuApp = isMenuApp + self.logger = logger + self.vpnSettings = vpnSettings + + guard !isMenuApp else { + return + } + + if #available(macOS 14.0, *) { + handleActiveSiteInfoChanged(newValue: activeSiteInfo) + handleConnectionStatusChanged(oldValue: connectionStatus, newValue: connectionStatus) + + subscribeToConnectionStatusChanges(statusObserver) + subscribeToFeatureFlagChanges(featureFlagPublisher) + subscribeToActiveSiteChanges(activeSitePublisher) + } + } + + deinit { + geoswitchingStatusUpdateTask?.cancel() + geoswitchingStatusUpdateTask = nil + } + + var canShowTips: Bool { + !isMenuApp && featureFlag + } + + // MARK: - Subscriptions + + @available(macOS 14.0, *) + private func subscribeToFeatureFlagChanges(_ publisher: CurrentValuePublisher) { + publisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .assign(to: \.featureFlag, onWeaklyHeld: self) + .store(in: &cancellables) + } + + @available(macOS 14.0, *) + private func subscribeToConnectionStatusChanges(_ statusObserver: ConnectionStatusObserver) { + statusObserver.publisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .assign(to: \.connectionStatus, onWeaklyHeld: self) + .store(in: &cancellables) + } + + @available(macOS 14.0, *) + private func subscribeToActiveSiteChanges(_ publisher: CurrentValuePublisher) { + + publisher + .removeDuplicates() + .receive(on: DispatchQueue.main) + .assign(to: \.activeSiteInfo, onWeaklyHeld: self) + .store(in: &cancellables) + } + + // MARK: - Tips + + let autoconnectTip = VPNAutoconnectTip() + let domainExclusionsTip = VPNDomainExclusionsTip() + let geoswitchingTip = VPNGeoswitchingTip() + + var geoswitchingStatusUpdateTask: Task? + + // MARK: - Tip Action handling + + @available(macOS 14.0, *) + func autoconnectTipActionHandler(_ action: Tip.Action) { + if action.id == VPNAutoconnectTip.ActionIdentifiers.enable.rawValue { + vpnSettings.connectOnLogin = true + + autoconnectTip.invalidate(reason: .actionPerformed) + } + } + + // MARK: - Handle Refreshing + + @available(macOS 14.0, *) + private func handleActiveSiteInfoChanged(newValue: ActiveSiteInfo?) { + guard !isMenuApp else { return } + return VPNDomainExclusionsTip.hasActiveSite = (activeSiteInfo != nil) + } + + @available(macOS 14.0, *) + private func handleConnectionStatusChanged(oldValue: ConnectionStatus, newValue: ConnectionStatus) { + guard !isMenuApp else { return } + switch newValue { + case .connected: + if case oldValue = .connecting { + handleTipDistanceConditionsCheckpoint() + } + + VPNGeoswitchingTip.vpnEnabledOnce = true + VPNAutoconnectTip.vpnEnabled = true + VPNDomainExclusionsTip.vpnEnabled = true + default: + VPNAutoconnectTip.vpnEnabled = false + VPNDomainExclusionsTip.vpnEnabled = false + } + } + + @available(macOS 14.0, *) + private func handleTipDistanceConditionsCheckpoint() { + if case .invalidated = geoswitchingTip.status { + VPNDomainExclusionsTip.isDistancedFromPreviousTip = true + } + + if case .invalidated = domainExclusionsTip.status { + VPNAutoconnectTip.isDistancedFromPreviousTip = true + } + } + + // MARK: - UI Events + + @available(macOS 14.0, *) + func handleAutoconnectTipInvalidated(_ reason: Tip.InvalidationReason) { + switch reason { + case .actionPerformed: + PixelKit.fire(VPNTipPixel.autoconnectTip(step: .actioned)) + default: + PixelKit.fire(VPNTipPixel.autoconnectTip(step: .dismissed)) + } + } + + @available(macOS 14.0, *) + func handleDomainExclusionTipInvalidated(_ reason: Tip.InvalidationReason) { + switch reason { + case .actionPerformed: + PixelKit.fire(VPNTipPixel.domainExclusionsTip(step: .actioned)) + default: + PixelKit.fire(VPNTipPixel.domainExclusionsTip(step: .dismissed)) + } + } + + @available(macOS 14.0, *) + func handleGeoswitchingTipInvalidated(_ reason: Tip.InvalidationReason) { + switch reason { + case .actionPerformed: + PixelKit.fire(VPNTipPixel.geoswitchingTip(step: .actioned)) + default: + PixelKit.fire(VPNTipPixel.geoswitchingTip(step: .dismissed)) + } + } + + @available(macOS 14.0, *) + func handleLocationsShown() { + guard !isMenuApp else { return } + geoswitchingTip.invalidate(reason: .actionPerformed) + } + + @available(macOS 14.0, *) + func handleSiteExcluded() { + guard !isMenuApp else { return } + domainExclusionsTip.invalidate(reason: .actionPerformed) + } + + @available(macOS 14.0, *) + func handleTunnelControllerAppear() { + guard !isMenuApp else { return } + + handleTipDistanceConditionsCheckpoint() + } + + @available(macOS 14.0, *) + func handleTunnelControllerDisappear() { + guard !isMenuApp else { return } + + if case .available = autoconnectTip.status { + PixelKit.fire(VPNTipPixel.autoconnectTip(step: .ignored)) + } + + if case .available = domainExclusionsTip.status { + PixelKit.fire(VPNTipPixel.domainExclusionsTip(step: .ignored)) + } + + if case .available = geoswitchingTip.status { + PixelKit.fire(VPNTipPixel.geoswitchingTip(step: .ignored)) + } + } + + @available(macOS 14.0, *) + func handleAutoconnectionTipShown() { + guard !isMenuApp else { return } + + PixelKit.fire(VPNTipPixel.autoconnectTip(step: .shown)) + } + + @available(macOS 14.0, *) + func handleDomainExclusionsTipShown() { + guard !isMenuApp else { return } + + PixelKit.fire(VPNTipPixel.domainExclusionsTip(step: .shown)) + } + + @available(macOS 14.0, *) + func handleGeoswitchingTipShown() { + guard !isMenuApp else { return } + + PixelKit.fire(VPNTipPixel.geoswitchingTip(step: .shown)) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index a75ea8f6b9..d786816b5c 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -21,6 +21,8 @@ import SwiftUIExtensions import Combine import NetworkProtection import Lottie +import os.log +import TipKit public struct TunnelControllerView: View { @@ -32,7 +34,8 @@ public struct TunnelControllerView: View { /// The view model that this instance will use. /// - @ObservedObject var model: TunnelControllerViewModel + @ObservedObject + var model: TunnelControllerViewModel // MARK: - Initializers @@ -40,6 +43,9 @@ public struct TunnelControllerView: View { self.model = model } + @EnvironmentObject + private var tipsModel: VPNTipsModel + // MARK: - View Contents public var body: some View { @@ -49,9 +55,63 @@ public struct TunnelControllerView: View { featureToggleRow() + if #available(macOS 14.0, *), + tipsModel.canShowTips, + case .invalidated = tipsModel.domainExclusionsTip.status { + + TipView(tipsModel.autoconnectTip, action: tipsModel.autoconnectTipActionHandler) + .tipImageSize(VPNTipsModel.imageSize) + .tipBackground(Color(.tipBackground)) + .padding(.horizontal, 9) + .padding(.vertical, 6) + .onAppear { + tipsModel.handleAutoconnectionTipShown() + } + .task { + var previousStatus = tipsModel.autoconnectTip.status + + for await status in tipsModel.autoconnectTip.statusUpdates { + if case .invalidated(let reason) = status { + if case .available = previousStatus { + tipsModel.handleAutoconnectTipInvalidated(reason) + } + } + + previousStatus = status + } + } + } + SiteTroubleshootingView() .padding(.top, 5) + if #available(macOS 14.0, *), + tipsModel.canShowTips, + case .invalidated = tipsModel.geoswitchingTip.status { + + TipView(tipsModel.domainExclusionsTip) + .tipImageSize(VPNTipsModel.imageSize) + .tipBackground(Color(.tipBackground)) + .padding(.horizontal, 9) + .padding(.vertical, 6) + .onAppear { + tipsModel.handleDomainExclusionsTipShown() + } + .task { + var previousStatus = tipsModel.domainExclusionsTip.status + + for await status in tipsModel.domainExclusionsTip.statusUpdates { + if case .invalidated(let reason) = status { + if case .available = previousStatus { + tipsModel.handleDomainExclusionTipInvalidated(reason) + } + } + + previousStatus = status + } + } + } + Divider() .padding(EdgeInsets(top: 5, leading: 9, bottom: 5, trailing: 9)) @@ -62,6 +122,16 @@ public struct TunnelControllerView: View { .disabled(on: !isEnabled) } } + .onAppear { + if #available(macOS 14.0, *) { + tipsModel.handleTunnelControllerAppear() + } + } + .onDisappear { + if #available(macOS 14.0, *) { + tipsModel.handleTunnelControllerDisappear() + } + } } // MARK: - Composite Views @@ -105,7 +175,7 @@ public struct TunnelControllerView: View { introEndFrame: 100, loopStartFrame: 130, loopEndFrame: 370 - ), isAnimating: $model.isVPNEnabled) + ), isAnimating: model.isVPNEnabled) } @ViewBuilder @@ -124,6 +194,10 @@ public struct TunnelControllerView: View { .padding(EdgeInsets(top: 6, leading: 9, bottom: 6, trailing: 9)) MenuItemCustomButton { + if #available(macOS 14.0, *) { + tipsModel.handleLocationsShown() + } + model.showLocationSettings() dismiss() } label: { isHovered in @@ -168,6 +242,32 @@ public struct TunnelControllerView: View { } } + if #available(macOS 14.0, *), + tipsModel.canShowTips { + + TipView(tipsModel.geoswitchingTip) + .tipImageSize(VPNTipsModel.imageSize) + .tipBackground(Color(.tipBackground)) + .padding(.horizontal, 9) + .padding(.vertical, 6) + .onAppear { + tipsModel.handleGeoswitchingTipShown() + } + .task { + var previousStatus = tipsModel.geoswitchingTip.status + + for await status in tipsModel.geoswitchingTip.statusUpdates { + if case .invalidated(let reason) = status { + if case .available = previousStatus { + tipsModel.handleGeoswitchingTipInvalidated(reason) + } + } + + previousStatus = status + } + } + } + dividerRow() } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index 0fa6932fff..b0945d7a6f 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -20,6 +20,7 @@ import Combine import Foundation import NetworkProtection import SwiftUI +import TipKit @MainActor public final class TunnelControllerViewModel: ObservableObject { @@ -34,8 +35,17 @@ public final class TunnelControllerViewModel: ObservableObject { /// Whether the VPN is enabled /// This is determined based on the connection status, same as the iOS version - @Published - public var isVPNEnabled = false + /// + public var isVPNEnabled: Bool { + get { + switch connectionStatus { + case .connected, .connecting: + return true + default: + return false + } + } + } /// The type of extension that's being used for NetP /// @@ -68,11 +78,6 @@ public final class TunnelControllerViewModel: ObservableObject { private let uiActionHandler: VPNUIActionHandling - // MARK: - Environment - - @EnvironmentObject - private var siteTroubleshootingViewModel: SiteTroubleshootingView.Model - // MARK: - Misc /// The `RunLoop` for the timer. @@ -80,13 +85,6 @@ public final class TunnelControllerViewModel: ObservableObject { private let runLoopMode: RunLoop.Mode? private var cancellables = Set() - // MARK: - Dispatch Queues - - private static let statusDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.statusDispatchQueue", qos: .userInteractive) - private static let connectivityIssuesDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.connectivityIssuesDispatchQueue", qos: .userInteractive) - private static let serverInfoDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.serverInfoDispatchQueue", qos: .userInteractive) - private static let dataVolumeDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.dataVolumeDispatchQueue", qos: .userInteractive) - // MARK: - Initialization & Deinitialization public init(controller: TunnelController, @@ -106,6 +104,8 @@ public final class TunnelControllerViewModel: ObservableObject { self.uiActionHandler = uiActionHandler connectionStatus = statusReporter.statusObserver.recentValue + dnsSettings = vpnSettings.dnsSettings + formattedDataVolume = statusReporter.dataVolumeObserver.recentValue.formatted(using: Self.byteCountFormatter) internalServerAddress = statusReporter.serverInfoObserver.recentValue.serverAddress internalServerAttributes = statusReporter.serverInfoObserver.recentValue.serverLocation @@ -140,29 +140,15 @@ public final class TunnelControllerViewModel: ObservableObject { private func subscribeToStatusChanges() { statusReporter.statusObserver.publisher - .subscribe(on: Self.statusDispatchQueue) - .sink { [weak self] status in - - guard let self else { - return - } - - Task { @MainActor in - self.connectionStatus = status - switch status { - case .connected, .connecting: - self.isVPNEnabled = true - default: - self.isVPNEnabled = false - } - } - } + .removeDuplicates() + .receive(on: DispatchQueue.main) + .assign(to: \.connectionStatus, onWeaklyHeld: self) .store(in: &cancellables) } private func subscribeToServerInfoChanges() { statusReporter.serverInfoObserver.publisher - .subscribe(on: Self.serverInfoDispatchQueue) + .receive(on: DispatchQueue.main) .sink { [weak self] serverInfo in guard let self else { @@ -180,7 +166,6 @@ public final class TunnelControllerViewModel: ObservableObject { private func subscribeToDataVolumeUpdates() { statusReporter.dataVolumeObserver.publisher - .subscribe(on: Self.dataVolumeDispatchQueue) .map { $0.formatted(using: Self.byteCountFormatter) } .receive(on: DispatchQueue.main) .assign(to: \.formattedDataVolume, onWeaklyHeld: self) @@ -290,7 +275,7 @@ public final class TunnelControllerViewModel: ObservableObject { @MainActor @Published - private var connectionStatus: NetworkProtection.ConnectionStatus = .default { + private var connectionStatus: NetworkProtection.ConnectionStatus { didSet { detectAndRefreshExternalToggleSwitching() previousConnectionStatus = oldValue @@ -302,6 +287,7 @@ public final class TunnelControllerViewModel: ObservableObject { /// This method serves as a simple mechanism to detect when the toggle is controlled by the agent app, or by another /// external event causing the tunnel to start or stop, so we can disable the toggle as it's transitioning.. /// + @MainActor private func detectAndRefreshExternalToggleSwitching() { switch toggleTransition { case .idle: @@ -325,7 +311,6 @@ public final class TunnelControllerViewModel: ObservableObject { // MARK: - Connection Status: Toggle State - @frozen enum ToggleTransition: Equatable { case idle case switchingOn(locallyInitiated: Bool) @@ -354,6 +339,7 @@ public final class TunnelControllerViewModel: ObservableObject { /// @Published var timeLapsed = UserText.networkProtectionStatusViewTimerZero + @MainActor private func refreshTimeLapsed() { switch connectionStatus { case .connected(let connectedDate): @@ -481,7 +467,7 @@ public final class TunnelControllerViewModel: ObservableObject { private var internalServerAttributes: NetworkProtectionServerInfo.ServerAttributes? @Published - var dnsSettings: NetworkProtectionDNSSettings = .default + var dnsSettings: NetworkProtectionDNSSettings @Published var formattedDataVolume: FormattedDataVolume diff --git a/LocalPackages/NetworkProtectionMac/Sources/VPNPixels/VPNTipPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/VPNPixels/VPNTipPixel.swift new file mode 100644 index 0000000000..99d314ec42 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/VPNPixels/VPNTipPixel.swift @@ -0,0 +1,52 @@ +// +// VPNTipPixel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import PixelKit + +public enum VPNTipStep: String { + case shown + case ignored + case actioned + case dismissed +} + +public enum VPNTipPixel: VPNPixel { + case autoconnectTip(step: VPNTipStep) + case domainExclusionsTip(step: VPNTipStep) + case geoswitchingTip(step: VPNTipStep) + + public var unscopedPixelName: String { + switch self { + case .autoconnectTip(let step): + return "tip_autoconnect_\(step)" + case .domainExclusionsTip(let step): + return "tip_site-exclusion_\(step)" + case .geoswitchingTip(let step): + return "tip_geoswitching_\(step)" + } + } + + public var error: (any Error)? { + nil + } + + public var parameters: [String: String]? { + nil + } +}