diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNTipsModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNTipsModel.swift index 46a8bc1152..fd7a2518dc 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNTipsModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TipViews/Model/VPNTipsModel.swift @@ -22,6 +22,8 @@ import Common import NetworkProtection import os.log import TipKit +import PixelKit +import VPNPixels @MainActor public final class VPNTipsModel: ObservableObject { @@ -183,14 +185,33 @@ public final class VPNTipsModel: ObservableObject { // 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: - Logger.networkProtection.log("🧉 Geo-switching tip actioned") - break + PixelKit.fire(VPNTipPixel.geoswitchingTip(step: .actioned)) default: - Logger.networkProtection.log("🧉 Geo-switching tip dismissed") + PixelKit.fire(VPNTipPixel.geoswitchingTip(step: .dismissed)) } } @@ -207,9 +228,47 @@ public final class VPNTipsModel: ObservableObject { } @available(macOS 14.0, *) - func handleTunnelControllerShown() { + 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 7754adab40..d786816b5c 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -64,6 +64,22 @@ public struct TunnelControllerView: View { .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() @@ -78,17 +94,20 @@ public struct TunnelControllerView: View { .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 = status { + if case .invalidated(let reason) = status { if case .available = previousStatus { - Logger.networkProtection.log("🧉 Domain exclusions tip dismissed") + tipsModel.handleDomainExclusionTipInvalidated(reason) } - - previousStatus = status } + + previousStatus = status } } } @@ -105,7 +124,12 @@ public struct TunnelControllerView: View { } .onAppear { if #available(macOS 14.0, *) { - tipsModel.handleTunnelControllerShown() + tipsModel.handleTunnelControllerAppear() + } + } + .onDisappear { + if #available(macOS 14.0, *) { + tipsModel.handleTunnelControllerDisappear() } } } @@ -226,6 +250,9 @@ public struct TunnelControllerView: View { .tipBackground(Color(.tipBackground)) .padding(.horizontal, 9) .padding(.vertical, 6) + .onAppear { + tipsModel.handleGeoswitchingTipShown() + } .task { var previousStatus = tipsModel.geoswitchingTip.status 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 + } +}