Skip to content

Commit

Permalink
Show VPN onboarding tips (#3410)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
diegoreymendez authored Nov 28, 2024
1 parent 5e7b16b commit 79f07a8
Show file tree
Hide file tree
Showing 35 changed files with 1,250 additions and 94 deletions.
70 changes: 64 additions & 6 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions DuckDuckGo/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

DataBrokerProtectionAppEvents(featureGatekeeper: pirGatekeeper).applicationDidFinishLaunching()

TipKitAppEventHandler(featureFlagger: featureFlagger).appDidFinishLaunching()

setUpAutoClearHandler()

setUpAutofillPixelReporter()
Expand Down
5 changes: 5 additions & 0 deletions DuckDuckGo/Menus/MainMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
4 changes: 4 additions & 0 deletions DuckDuckGo/Menus/MainMenuActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// SiteTroubleshootingInfoPublisher.swift
// ActiveSiteInfoPublisher.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
Expand All @@ -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<SiteTroubleshootingInfo?, Never>
private let subject: CurrentValueSubject<ActiveSiteInfo?, Never>

private let activeDomainPublisher: AnyPublisher<String?, Never>
private let proxySettings: TransparentProxySettings
Expand All @@ -39,7 +39,7 @@ final class SiteTroubleshootingInfoPublisher {
init(activeDomainPublisher: AnyPublisher<String?, Never>,
proxySettings: TransparentProxySettings) {

subject = CurrentValueSubject<SiteTroubleshootingInfo?, Never>(nil)
subject = CurrentValueSubject<ActiveSiteInfo?, Never>(nil)
self.activeDomainPublisher = activeDomainPublisher
self.proxySettings = proxySettings

Expand All @@ -59,7 +59,7 @@ final class SiteTroubleshootingInfoPublisher {

switch change {
case .excludedDomains:
refreshSiteTroubleshootingInfo()
refreshActiveSiteInfo()
default:
break
}
Expand All @@ -68,29 +68,29 @@ 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
}

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))
Expand All @@ -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<S>(subscriber: S) where S: Subscriber, Never == S.Failure, NetworkProtectionUI.SiteTroubleshootingInfo? == S.Input {
func receive<S>(subscriber: S) where S: Subscriber, Never == S.Failure, NetworkProtectionUI.ActiveSiteInfo? == S.Input {

subject.receive(subscriber: subscriber)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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<AnyCancellable>()

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

let activeDomainPublisher = ActiveDomainPublisher(windowControllersManager: .shared)

siteTroubleshootingInfoPublisher = SiteTroubleshootingInfoPublisher(
activeSitePublisher = ActiveSiteInfoPublisher(
activeDomainPublisher: activeDomainPublisher.eraseToAnyPublisher(),
proxySettings: TransparentProxySettings(defaults: .netP))

subscribeToCurrentSitePublisher()
}

private func subscribeToCurrentSitePublisher() {
siteTroubleshootingInfoPublisher
activeSitePublisher
.assign(to: \.siteInfo, onWeaklyHeld: self)
.store(in: &cancellables)
}
Expand All @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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<Bool, Never>

if let overridesHandler = featureFlagger.localOverrides?.actionHandler as? FeatureFlagOverridesPublishingHandler<FeatureFlag> {

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

Expand Down
24 changes: 22 additions & 2 deletions DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -49,8 +57,6 @@ final class VPNPreferencesModel: ObservableObject {
}
}

@Published var secureDNS: Bool = true

@Published var showInMenuBar: Bool {
didSet {
settings.showInMenuBar = showInMenuBar
Expand Down Expand Up @@ -117,6 +123,8 @@ final class VPNPreferencesModel: ObservableObject {
locationItem = VPNLocationPreferenceItemModel(selectedLocation: settings.selectedLocation)

subscribeToOnboardingStatusChanges(defaults: defaults)
subscribeToConnectOnLoginSettingChanges()
subscribeToExcludeLocalNetworksSettingChanges()
subscribeToShowInMenuBarSettingChanges()
subscribeToShowInBrowserToolbarSettingsChanges()
subscribeToLocationSettingChanges()
Expand All @@ -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()
Expand Down
27 changes: 27 additions & 0 deletions DuckDuckGo/TipKit/Logger+TipKit.swift
Original file line number Diff line number Diff line change
@@ -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")
}()
}
Loading

0 comments on commit 79f07a8

Please sign in to comment.