Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit 79f07a8

Browse files
Show VPN onboarding tips (#3410)
Task/Issue URL: https://app.asana.com/0/1206580121312550/1208795272851000/f iOS PR: duckduckgo/iOS#3429 BSK PR: duckduckgo/BrowserServicesKit#1024 ## Description Shows VPN onboarding tips.
1 parent 5e7b16b commit 79f07a8

35 files changed

+1250
-94
lines changed

DuckDuckGo.xcodeproj/project.pbxproj

+64-6
Large diffs are not rendered by default.

DuckDuckGo/Application/AppDelegate.swift

+2
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
468468

469469
DataBrokerProtectionAppEvents(featureGatekeeper: pirGatekeeper).applicationDidFinishLaunching()
470470

471+
TipKitAppEventHandler(featureFlagger: featureFlagger).appDidFinishLaunching()
472+
471473
setUpAutoClearHandler()
472474

473475
setUpAutofillPixelReporter()

DuckDuckGo/Menus/MainMenu.swift

+5
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,11 @@ final class MainMenu: NSMenu {
728728
openSubscriptionTab: { WindowControllersManager.shared.showTab(with: .subscription($0)) },
729729
subscriptionManager: Application.appDelegate.subscriptionManager)
730730

731+
NSMenuItem(title: "TipKit") {
732+
NSMenuItem(title: "Reset", action: #selector(MainViewController.resetTipKit))
733+
NSMenuItem(title: "⚠️ App restart required.", action: nil, target: nil)
734+
}
735+
731736
NSMenuItem(title: "Logging").submenu(setupLoggingMenu())
732737
NSMenuItem(title: "AI Chat").submenu(AIChatDebugMenu())
733738

DuckDuckGo/Menus/MainMenuActions.swift

+4
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,10 @@ extension MainViewController {
901901
SyncPromoManager().resetPromos()
902902
}
903903

904+
@objc func resetTipKit(_ sender: Any?) {
905+
TipKitDebugOptionsUIActionHandler().resetTipKitTapped()
906+
}
907+
904908
@objc func internalUserState(_ sender: Any?) {
905909
guard let internalUserDecider = NSApp.delegateTyped.internalUserDecider as? DefaultInternalUserDecider else { return }
906910
let state = internalUserDecider.isInternalUser

DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/ActiveDomainPublisher.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ final class ActiveDomainPublisher {
4343
}
4444
}
4545

46+
@MainActor
4647
init(windowControllersManager: WindowControllersManager) {
48+
49+
if let tabContent = windowControllersManager.lastKeyMainWindowController?.activeTab?.content {
50+
activeDomain = Self.domain(from: tabContent)
51+
}
52+
4753
self.windowControllersManager = windowControllersManager
4854

4955
Task { @MainActor in
@@ -73,7 +79,7 @@ final class ActiveDomainPublisher {
7379
@MainActor
7480
private func subscribeToActiveTabContentChanges() {
7581
activeTabContentCancellable = activeTab?.$content
76-
.map(domain(from:))
82+
.map(Self.domain(from:))
7783
.removeDuplicates()
7884
.assign(to: \.activeDomain, onWeaklyHeld: self)
7985
}
@@ -88,7 +94,7 @@ final class ActiveDomainPublisher {
8894
}
8995
}
9096

91-
private func domain(from tabContent: Tab.TabContent) -> String? {
97+
private static func domain(from tabContent: Tab.TabContent) -> String? {
9298
if case .url(let url, _, _) = tabContent {
9399

94100
return url.host

DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/SiteTroubleshootingInfoPublisher.swift DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/ActiveSiteInfoPublisher.swift

+16-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// SiteTroubleshootingInfoPublisher.swift
2+
// ActiveSiteInfoPublisher.swift
33
//
44
// Copyright © 2024 DuckDuckGo. All rights reserved.
55
//
@@ -22,15 +22,15 @@ import NetworkProtectionProxy
2222
import NetworkProtectionUI
2323

2424
@MainActor
25-
final class SiteTroubleshootingInfoPublisher {
25+
final class ActiveSiteInfoPublisher {
2626

2727
private var activeDomain: String? {
2828
didSet {
29-
refreshSiteTroubleshootingInfo()
29+
refreshActiveSiteInfo()
3030
}
3131
}
3232

33-
private let subject: CurrentValueSubject<SiteTroubleshootingInfo?, Never>
33+
private let subject: CurrentValueSubject<ActiveSiteInfo?, Never>
3434

3535
private let activeDomainPublisher: AnyPublisher<String?, Never>
3636
private let proxySettings: TransparentProxySettings
@@ -39,7 +39,7 @@ final class SiteTroubleshootingInfoPublisher {
3939
init(activeDomainPublisher: AnyPublisher<String?, Never>,
4040
proxySettings: TransparentProxySettings) {
4141

42-
subject = CurrentValueSubject<SiteTroubleshootingInfo?, Never>(nil)
42+
subject = CurrentValueSubject<ActiveSiteInfo?, Never>(nil)
4343
self.activeDomainPublisher = activeDomainPublisher
4444
self.proxySettings = proxySettings
4545

@@ -59,7 +59,7 @@ final class SiteTroubleshootingInfoPublisher {
5959

6060
switch change {
6161
case .excludedDomains:
62-
refreshSiteTroubleshootingInfo()
62+
refreshActiveSiteInfo()
6363
default:
6464
break
6565
}
@@ -68,29 +68,29 @@ final class SiteTroubleshootingInfoPublisher {
6868

6969
// MARK: - Refreshing
7070

71-
func refreshSiteTroubleshootingInfo() {
72-
if activeSiteTroubleshootingInfo != subject.value {
73-
subject.send(activeSiteTroubleshootingInfo)
71+
func refreshActiveSiteInfo() {
72+
if activeActiveSiteInfo != subject.value {
73+
subject.send(activeActiveSiteInfo)
7474
}
7575
}
7676

7777
// MARK: - Active Site Troubleshooting Info
7878

79-
var activeSiteTroubleshootingInfo: SiteTroubleshootingInfo? {
79+
var activeActiveSiteInfo: ActiveSiteInfo? {
8080
guard let activeDomain else {
8181
return nil
8282
}
8383

8484
return site(forDomain: activeDomain.droppingWwwPrefix())
8585
}
8686

87-
private func site(forDomain domain: String) -> SiteTroubleshootingInfo? {
87+
private func site(forDomain domain: String) -> ActiveSiteInfo? {
8888
let icon: NSImage?
89-
let currentSite: NetworkProtectionUI.SiteTroubleshootingInfo?
89+
let currentSite: NetworkProtectionUI.ActiveSiteInfo?
9090

9191
icon = FaviconManager.shared.getCachedFavicon(forDomainOrAnySubdomain: domain, sizeCategory: .small)?.image
9292
let proxySettings = TransparentProxySettings(defaults: .netP)
93-
currentSite = NetworkProtectionUI.SiteTroubleshootingInfo(
93+
currentSite = NetworkProtectionUI.ActiveSiteInfo(
9494
icon: icon,
9595
domain: domain,
9696
excluded: proxySettings.isExcluding(domain: domain))
@@ -99,12 +99,12 @@ final class SiteTroubleshootingInfoPublisher {
9999
}
100100
}
101101

102-
extension SiteTroubleshootingInfoPublisher: Publisher {
103-
typealias Output = SiteTroubleshootingInfo?
102+
extension ActiveSiteInfoPublisher: Publisher {
103+
typealias Output = ActiveSiteInfo?
104104
typealias Failure = Never
105105

106106
nonisolated
107-
func receive<S>(subscriber: S) where S: Subscriber, Never == S.Failure, NetworkProtectionUI.SiteTroubleshootingInfo? == S.Input {
107+
func receive<S>(subscriber: S) where S: Subscriber, Never == S.Failure, NetworkProtectionUI.ActiveSiteInfo? == S.Input {
108108

109109
subject.receive(subscriber: subscriber)
110110
}

DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift

+47-10
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@ import Foundation
2424
import LoginItems
2525
import NetworkProtection
2626
import NetworkProtectionIPC
27+
import NetworkProtectionProxy
2728
import NetworkProtectionUI
29+
import os.log
2830
import Subscription
29-
import VPNAppLauncher
3031
import SwiftUI
31-
import NetworkProtectionProxy
32+
import VPNAppLauncher
33+
import BrowserServicesKit
34+
import FeatureFlags
3235

3336
protocol NetworkProtectionIPCClient {
3437
var ipcStatusObserver: ConnectionStatusObserver { get }
@@ -55,8 +58,8 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager {
5558
let vpnUninstaller: VPNUninstalling
5659

5760
@Published
58-
private var siteInfo: SiteTroubleshootingInfo?
59-
private let siteTroubleshootingInfoPublisher: SiteTroubleshootingInfoPublisher
61+
private var siteInfo: ActiveSiteInfo?
62+
private let activeSitePublisher: ActiveSiteInfoPublisher
6063
private var cancellables = Set<AnyCancellable>()
6164

6265
init(ipcClient: VPNControllerXPCClient,
@@ -67,15 +70,15 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager {
6770

6871
let activeDomainPublisher = ActiveDomainPublisher(windowControllersManager: .shared)
6972

70-
siteTroubleshootingInfoPublisher = SiteTroubleshootingInfoPublisher(
73+
activeSitePublisher = ActiveSiteInfoPublisher(
7174
activeDomainPublisher: activeDomainPublisher.eraseToAnyPublisher(),
7275
proxySettings: TransparentProxySettings(defaults: .netP))
7376

7477
subscribeToCurrentSitePublisher()
7578
}
7679

7780
private func subscribeToCurrentSitePublisher() {
78-
siteTroubleshootingInfoPublisher
81+
activeSitePublisher
7982
.assign(to: \.siteInfo, onWeaklyHeld: self)
8083
.store(in: &cancellables)
8184
}
@@ -87,9 +90,10 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager {
8790
func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) -> NSPopover {
8891

8992
/// Since the favicon doesn't have a publisher we force refreshing here
90-
siteTroubleshootingInfoPublisher.refreshSiteTroubleshootingInfo()
93+
activeSitePublisher.refreshActiveSiteInfo()
9194

9295
let popover: NSPopover = {
96+
let vpnSettings = VPNSettings(defaults: .netP)
9397
let controller = NetworkProtectionIPCTunnelController(ipcClient: ipcClient)
9498

9599
let statusReporter = DefaultNetworkProtectionStatusReporter(
@@ -103,15 +107,22 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager {
103107
)
104108

105109
let onboardingStatusPublisher = UserDefaults.netP.networkProtectionOnboardingStatusPublisher
106-
_ = VPNSettings(defaults: .netP)
107110
let appLauncher = AppLauncher(appBundleURL: Bundle.main.bundleURL)
108111
let vpnURLEventHandler = VPNURLEventHandler()
109112
let proxySettings = TransparentProxySettings(defaults: .netP)
110113
let uiActionHandler = VPNUIActionHandler(vpnURLEventHandler: vpnURLEventHandler, proxySettings: proxySettings)
111114

115+
let connectionStatusPublisher = CurrentValuePublisher(
116+
initialValue: statusReporter.statusObserver.recentValue,
117+
publisher: statusReporter.statusObserver.publisher)
118+
119+
let activeSitePublisher = CurrentValuePublisher(
120+
initialValue: siteInfo,
121+
publisher: $siteInfo.eraseToAnyPublisher())
122+
112123
let siteTroubleshootingViewModel = SiteTroubleshootingView.Model(
113-
connectionStatusPublisher: statusReporter.statusObserver.publisher,
114-
siteTroubleshootingInfoPublisher: $siteInfo.eraseToAnyPublisher(),
124+
connectionStatusPublisher: connectionStatusPublisher,
125+
activeSitePublisher: activeSitePublisher,
115126
uiActionHandler: uiActionHandler)
116127

117128
let statusViewModel = NetworkProtectionStatusView.Model(controller: controller,
@@ -157,10 +168,36 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager {
157168
_ = try? await self?.vpnUninstaller.uninstall(removeSystemExtension: true)
158169
})
159170

171+
let featureFlagger = NSApp.delegateTyped.featureFlagger
172+
let tipsFeatureFlagInitialValue = featureFlagger.isFeatureOn(.networkProtectionUserTips)
173+
let tipsFeatureFlagPublisher: CurrentValuePublisher<Bool, Never>
174+
175+
if let overridesHandler = featureFlagger.localOverrides?.actionHandler as? FeatureFlagOverridesPublishingHandler<FeatureFlag> {
176+
177+
let featureFlagPublisher = overridesHandler.flagDidChangePublisher
178+
.filter { $0.0 == .networkProtectionUserTips }
179+
180+
tipsFeatureFlagPublisher = CurrentValuePublisher(
181+
initialValue: tipsFeatureFlagInitialValue,
182+
publisher: Just(tipsFeatureFlagInitialValue).eraseToAnyPublisher())
183+
} else {
184+
tipsFeatureFlagPublisher = CurrentValuePublisher(
185+
initialValue: tipsFeatureFlagInitialValue,
186+
publisher: Just(tipsFeatureFlagInitialValue).eraseToAnyPublisher())
187+
}
188+
189+
let tipsModel = VPNTipsModel(featureFlagPublisher: tipsFeatureFlagPublisher,
190+
statusObserver: statusReporter.statusObserver,
191+
activeSitePublisher: activeSitePublisher,
192+
forMenuApp: false,
193+
vpnSettings: vpnSettings,
194+
logger: Logger(subsystem: "DuckDuckGo", category: "TipKit"))
195+
160196
let popover = NetworkProtectionPopover(
161197
statusViewModel: statusViewModel,
162198
statusReporter: statusReporter,
163199
siteTroubleshootingViewModel: siteTroubleshootingViewModel,
200+
tipsModel: tipsModel,
164201
debugInformationViewModel: DebugInformationViewModel(showDebugInformation: false))
165202
popover.delegate = delegate
166203

DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift

+22-2
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,20 @@ final class VPNPreferencesModel: ObservableObject {
3232

3333
@Published var connectOnLogin: Bool {
3434
didSet {
35+
guard settings.connectOnLogin != connectOnLogin else {
36+
return
37+
}
38+
3539
settings.connectOnLogin = connectOnLogin
3640
}
3741
}
3842

3943
@Published var excludeLocalNetworks: Bool {
4044
didSet {
45+
guard settings.excludeLocalNetworks != excludeLocalNetworks else {
46+
return
47+
}
48+
4149
settings.excludeLocalNetworks = excludeLocalNetworks
4250

4351
Task {
@@ -49,8 +57,6 @@ final class VPNPreferencesModel: ObservableObject {
4957
}
5058
}
5159

52-
@Published var secureDNS: Bool = true
53-
5460
@Published var showInMenuBar: Bool {
5561
didSet {
5662
settings.showInMenuBar = showInMenuBar
@@ -117,6 +123,8 @@ final class VPNPreferencesModel: ObservableObject {
117123
locationItem = VPNLocationPreferenceItemModel(selectedLocation: settings.selectedLocation)
118124

119125
subscribeToOnboardingStatusChanges(defaults: defaults)
126+
subscribeToConnectOnLoginSettingChanges()
127+
subscribeToExcludeLocalNetworksSettingChanges()
120128
subscribeToShowInMenuBarSettingChanges()
121129
subscribeToShowInBrowserToolbarSettingsChanges()
122130
subscribeToLocationSettingChanges()
@@ -129,6 +137,18 @@ final class VPNPreferencesModel: ObservableObject {
129137
.store(in: &cancellables)
130138
}
131139

140+
func subscribeToConnectOnLoginSettingChanges() {
141+
settings.connectOnLoginPublisher
142+
.assign(to: \.connectOnLogin, onWeaklyHeld: self)
143+
.store(in: &cancellables)
144+
}
145+
146+
func subscribeToExcludeLocalNetworksSettingChanges() {
147+
settings.excludeLocalNetworksPublisher
148+
.assign(to: \.excludeLocalNetworks, onWeaklyHeld: self)
149+
.store(in: &cancellables)
150+
}
151+
132152
func subscribeToShowInMenuBarSettingChanges() {
133153
settings.showInMenuBarPublisher
134154
.removeDuplicates()

DuckDuckGo/TipKit/Logger+TipKit.swift

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// Logger+TipKit.swift
3+
//
4+
// Copyright © 2024 DuckDuckGo. All rights reserved.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
import Foundation
20+
import os.log
21+
22+
extension Logger {
23+
24+
static var tipKit: Logger = {
25+
Logger(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "TipKit")
26+
}()
27+
}

0 commit comments

Comments
 (0)