Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show VPN onboarding tips #3410

Merged
merged 35 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1beb4c9
WIP
diegoreymendez Oct 15, 2024
12c6d00
WIP
diegoreymendez Oct 17, 2024
c5723b2
WIP
diegoreymendez Oct 17, 2024
9c54287
WIP
diegoreymendez Oct 17, 2024
59b9348
Renames a few classes to improve readability
diegoreymendez Oct 17, 2024
2492fee
Makes several improvements to the code so that some publishers offer …
diegoreymendez Oct 17, 2024
80b97e0
WIP
diegoreymendez Oct 17, 2024
5d0687f
Updated BSK
diegoreymendez Oct 17, 2024
3c6260f
Changed the geolocation tip icon
diegoreymendez Oct 17, 2024
e6ccb08
Updated icons for VPN onboarding tips
diegoreymendez Oct 17, 2024
607feb0
WIP
diegoreymendez Oct 22, 2024
aa937d5
WIP
diegoreymendez Oct 22, 2024
7d0404a
WIP
diegoreymendez Oct 24, 2024
f5b3b71
WIP
diegoreymendez Oct 28, 2024
83899f3
WIP
diegoreymendez Oct 28, 2024
a2fa5dc
WIP
diegoreymendez Oct 29, 2024
9ca31ec
WIP
diegoreymendez Nov 19, 2024
31f022c
Merge branch 'main' into diego/add-vpn-tips
diegoreymendez Nov 19, 2024
865a5a7
Fixes TipKit on macOS. More fixes coming up.
diegoreymendez Nov 19, 2024
d807463
WIP
diegoreymendez Nov 19, 2024
a32cb01
Rolls back some unintentional changes
diegoreymendez Nov 19, 2024
a63b154
WIP
diegoreymendez Nov 19, 2024
ed60c48
Pushes additional fixes
diegoreymendez Nov 20, 2024
bf91f74
Addresses an issue with the code to handle the TipKit feature flag
diegoreymendez Nov 20, 2024
3ae5700
Fixes swiftlint warnings
diegoreymendez Nov 20, 2024
18b2b7a
Rolls back an unintentional change
diegoreymendez Nov 20, 2024
986777c
Makes some changes to improve TipKit support
diegoreymendez Nov 21, 2024
112a301
Fixes some issues with VPN tips
diegoreymendez Nov 21, 2024
cd57672
Fixes several UI issues in the new VPN tips
diegoreymendez Nov 22, 2024
3554610
Darkens the tip background
diegoreymendez Nov 22, 2024
6d99f53
Updates the VPN tips background color
diegoreymendez Nov 25, 2024
36d251c
Updates the tip background color
diegoreymendez Nov 25, 2024
5a77984
Fixes the conditions for showing our VPN tips
diegoreymendez Nov 26, 2024
5cd2212
Implements pixels for our VPN tips
diegoreymendez Nov 26, 2024
d1bb000
Updates some log messages that were off.
diegoreymendez Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -463,6 +463,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 @@ -723,6 +723,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)
}
Comment on lines +726 to +729
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convenience menu to test TipKit.


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 @@ -880,6 +880,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
samsymons marked this conversation as resolved.
Show resolved Hide resolved
//
// 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())
}
Comment on lines +171 to +187
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all to handle the feature flag. The CurrentValuePublisher needs an initial value, and a publisher for subsequent values.

I think some of this can be simplified in the feature flag handling logic, as it's mostly boilerplate. But here I wasn't too worried because FF code is temporary and this will go away anyway.

I can try to work on this if we see value, though.


let tipsModel = VPNTipsModel(featureFlagPublisher: tipsFeatureFlagPublisher,
statusObserver: statusReporter.statusObserver,
activeSitePublisher: activeSitePublisher,
forMenuApp: false,
vpnSettings: vpnSettings,
logger: Logger(subsystem: "DuckDuckGo", category: "TipKit"))
Comment on lines +189 to +194
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new model for handling the tips.


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
}
Comment on lines +35 to +37
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent an infinite loop when the change is coming from a different place. I actually got this to happen when enabling connect on login from the tips.


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()
Comment on lines +126 to +127
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were missing so if settings was open, the checkboxes weren't refreshing live.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Damn, really good catch - thanks!

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
Loading