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

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
70 changes: 64 additions & 6 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
{
"identity" : "lottie-spm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/airbnb/lottie-spm",
"location" : "https://github.com/airbnb/lottie-spm.git",
"state" : {
"revision" : "1d29eccc24cc8b75bff9f6804155112c0ffc9605",
"version" : "4.4.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
buildConfiguration = "CI"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
dataBrokerProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents()
DataBrokerProtectionAppEvents(featureGatekeeper: DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: subscriptionManager.accountManager)).applicationDidFinishLaunching()

TipKitAppEventHandler().appDidFinishLaunching()

setUpAutoClearHandler()

setUpAutofillPixelReporter()
Expand Down
16 changes: 8 additions & 8 deletions DuckDuckGo/InfoPlist.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"state" : "needs_review",
"value" : "DuckDuckGo"
}
},
Expand All @@ -19,43 +19,43 @@
},
"es" : {
"stringUnit" : {
"state" : "translated",
"state" : "needs_review",
"value" : "DuckDuckGo"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"state" : "needs_review",
"value" : "DuckDuckGo"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"state" : "needs_review",
"value" : "DuckDuckGo"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"state" : "needs_review",
"value" : "DuckDuckGo"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"state" : "needs_review",
"value" : "DuckDuckGo"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
"state" : "needs_review",
"value" : "DuckDuckGo"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"state" : "needs_review",
"value" : "DuckDuckGo"
}
}
Expand Down
4 changes: 2 additions & 2 deletions DuckDuckGo/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -37402,7 +37402,7 @@
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒"
"value" : "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible 🔒"
}
},
"es" : {
Expand Down Expand Up @@ -37462,7 +37462,7 @@
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "You’re all set!\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒"
"value" : "You’re all set!\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible 🔒"
}
},
"es" : {
Expand Down
5 changes: 5 additions & 0 deletions DuckDuckGo/Menus/MainMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,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())
}
debugMenu.addItem(internalUserItem)
Expand Down
4 changes: 4 additions & 0 deletions DuckDuckGo/Menus/MainMenuActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,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
@@ -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,12 @@ 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

protocol NetworkProtectionIPCClient {
var ipcStatusObserver: ConnectionStatusObserver { get }
Expand All @@ -54,8 +55,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 @@ -66,15 +67,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 @@ -86,9 +87,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 @@ -102,15 +104,18 @@ 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 activeSitePublisher = CurrentValuePublisher(
initialValue: nil,
publisher: $siteInfo.eraseToAnyPublisher())

let siteTroubleshootingViewModel = SiteTroubleshootingView.Model(
connectionStatusPublisher: statusReporter.statusObserver.publisher,
siteTroubleshootingInfoPublisher: $siteInfo.eraseToAnyPublisher(),
activeSitePublisher: activeSitePublisher.eraseToAnyPublisher(),
uiActionHandler: uiActionHandler)

let statusViewModel = NetworkProtectionStatusView.Model(controller: controller,
Expand Down Expand Up @@ -156,10 +161,21 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager {
_ = try? await self?.vpnUninstaller.uninstall(removeSystemExtension: true)
})

// TODO: replace with access to actual feature flag
let tipsFeatureFlagPublisher = CurrentValuePublisher(initialValue: true, publisher: Just(true).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
28 changes: 28 additions & 0 deletions DuckDuckGo/TipKit/Logger+TipKit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// Logger+TipKit.swift
// DuckDuckGo
//
// 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