Revisit overuse of EnvironmentObject (#794)

The biggest issue is the hidden and scattered use of both Tunnel and
ConnectionObserver. Only use the latter, and rename it to ExtendedTunnel
for being now a full wrapper around Tunnel (e.g. for .connectionStatus).

In general, restrict the use of EnvironmentObject to:

- Theme
- IAPManager
- ProfileProcessor
- ProviderManager

Always be explicit about:

- ProfileManager
- ExtendedTunnel

Contextually, move some UI entities to the base AppUI target.
This commit is contained in:
Davide 2024-11-01 09:47:50 +01:00 committed by GitHub
parent 33d238270e
commit 590b2790fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 200 additions and 223 deletions

View File

@ -83,10 +83,14 @@ extension PassepartoutApp {
.withEnvironment(from: context, theme: theme) .withEnvironment(from: context, theme: theme)
} }
MenuBarExtra { MenuBarExtra {
AppMenu() AppMenu(
.withEnvironment(from: context, theme: theme) profileManager: context.profileManager,
profileProcessor: context.profileProcessor,
tunnel: context.tunnel
)
.withEnvironment(from: context, theme: theme)
} label: { } label: {
AppMenuImage(connectionObserver: context.connectionObserver) AppMenuImage(tunnel: context.tunnel)
.environmentObject(theme) .environmentObject(theme)
} }
} }

View File

@ -38,12 +38,10 @@ public final class AppContext: ObservableObject {
public let profileProcessor: ProfileProcessor public let profileProcessor: ProfileProcessor
public let tunnel: Tunnel public let tunnel: ExtendedTunnel
public let tunnelEnvironment: TunnelEnvironment public let tunnelEnvironment: TunnelEnvironment
public let connectionObserver: ConnectionObserver
public let registry: Registry public let registry: Registry
public let providerManager: ProviderManager public let providerManager: ProviderManager
@ -65,9 +63,8 @@ public final class AppContext: ObservableObject {
self.iapManager = iapManager self.iapManager = iapManager
self.profileManager = profileManager self.profileManager = profileManager
self.profileProcessor = profileProcessor self.profileProcessor = profileProcessor
self.tunnel = tunnel
self.tunnelEnvironment = tunnelEnvironment self.tunnelEnvironment = tunnelEnvironment
connectionObserver = ConnectionObserver( self.tunnel = ExtendedTunnel(
tunnel: tunnel, tunnel: tunnel,
environment: tunnelEnvironment, environment: tunnelEnvironment,
interval: constants.tunnel.refreshInterval interval: constants.tunnel.refreshInterval
@ -79,7 +76,7 @@ public final class AppContext: ObservableObject {
Task { Task {
await iapManager.reloadReceipt() await iapManager.reloadReceipt()
connectionObserver.observeObjects() self.tunnel.observeObjects()
profileManager.observeObjects() profileManager.observeObjects()
observeObjects() observeObjects()
} }

View File

@ -1,5 +1,5 @@
// //
// ConnectionObserver.swift // ExtendedTunnel.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 9/7/24. // Created by Davide De Rosa on 9/7/24.
@ -29,8 +29,8 @@ import Foundation
import PassepartoutKit import PassepartoutKit
@MainActor @MainActor
public final class ConnectionObserver: ObservableObject { public final class ExtendedTunnel: ObservableObject {
public let tunnel: Tunnel private let tunnel: Tunnel
private let environment: TunnelEnvironment private let environment: TunnelEnvironment
@ -40,14 +40,10 @@ public final class ConnectionObserver: ObservableObject {
environment.environmentValue(forKey: key) environment.environmentValue(forKey: key)
} }
public var connectionStatus: ConnectionStatus? {
value(forKey: TunnelEnvironmentKeys.connectionStatus)
}
@Published @Published
public private(set) var lastErrorCode: PassepartoutError.Code? { public private(set) var lastErrorCode: PassepartoutError.Code? {
didSet { didSet {
pp_log(.app, .info, "ConnectionObserver.lastErrorCode -> \(lastErrorCode?.rawValue ?? "nil")") pp_log(.app, .info, "ExtendedTunnel.lastErrorCode -> \(lastErrorCode?.rawValue ?? "nil")")
} }
} }
@ -103,3 +99,63 @@ public final class ConnectionObserver: ObservableObject {
.store(in: &subscriptions) .store(in: &subscriptions)
} }
} }
extension ExtendedTunnel {
public var status: TunnelStatus {
tunnel.status
}
public var connectionStatus: TunnelStatus {
var status = tunnel.status
if status == .active, let environmentConnectionStatus {
if environmentConnectionStatus == .connected {
status = .active
} else {
status = .activating
}
}
return status
}
private var environmentConnectionStatus: ConnectionStatus? {
value(forKey: TunnelEnvironmentKeys.connectionStatus)
}
}
extension ExtendedTunnel {
public var currentProfile: TunnelCurrentProfile? {
tunnel.currentProfile
}
public func prepare(purge: Bool) async throws {
try await tunnel.prepare(purge: purge)
}
public func install(_ profile: Profile, processor: ProfileProcessor) async throws {
let newProfile = try processor.processed(profile)
try await tunnel.install(newProfile, connect: false, title: processor.title)
}
public func connect(with profile: Profile, processor: ProfileProcessor) async throws {
let newProfile = try processor.processed(profile)
try await tunnel.install(newProfile, connect: true, title: processor.title)
}
public func disconnect() async throws {
try await tunnel.disconnect()
}
public func currentLog(parameters: Constants.Log) async -> [String] {
let output = try? await tunnel.sendMessage(.localLog(
sinceLast: parameters.sinceLast,
maxLevel: parameters.maxLevel
))
switch output {
case .debugLog(let log):
return log.lines.map(parameters.formatter.formattedLine)
default:
return []
}
}
}

View File

@ -27,23 +27,26 @@ import Foundation
import PassepartoutKit import PassepartoutKit
@MainActor @MainActor
final class InteractiveManager: ObservableObject { public final class InteractiveManager: ObservableObject {
typealias CompletionBlock = (Profile) async throws -> Void public typealias CompletionBlock = (Profile) async throws -> Void
@Published @Published
var isPresented = false public var isPresented = false
private(set) var editor = ProfileEditor() public private(set) var editor = ProfileEditor()
private var onComplete: CompletionBlock? private var onComplete: CompletionBlock?
func present(with profile: Profile, onComplete: CompletionBlock?) { public init() {
}
public func present(with profile: Profile, onComplete: CompletionBlock?) {
editor = ProfileEditor(profile: profile) editor = ProfileEditor(profile: profile)
self.onComplete = onComplete self.onComplete = onComplete
isPresented = true isPresented = true
} }
func complete() async throws { public func complete() async throws {
isPresented = false isPresented = false
let newProfile = try editor.build() let newProfile = try editor.build()
try await onComplete?(newProfile) try await onComplete?(newProfile)

View File

@ -1,55 +0,0 @@
//
// Tunnel+Extensions.swift
// Passepartout
//
// Created by Davide De Rosa on 8/11/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import CommonLibrary
import Foundation
import PassepartoutKit
@MainActor
extension Tunnel {
public func install(_ profile: Profile, processor: ProfileProcessor) async throws {
let newProfile = try processor.processed(profile)
try await install(newProfile, connect: false, title: processor.title)
}
public func connect(with profile: Profile, processor: ProfileProcessor) async throws {
let newProfile = try processor.processed(profile)
try await install(newProfile, connect: true, title: processor.title)
}
public func currentLog(parameters: Constants.Log) async -> [String] {
let output = try? await sendMessage(.localLog(
sinceLast: parameters.sinceLast,
maxLevel: parameters.maxLevel
))
switch output {
case .debugLog(let log):
return log.lines.map(parameters.formatter.formattedLine)
default:
return []
}
}
}

View File

@ -30,12 +30,9 @@ import SwiftUI
extension View { extension View {
public func withEnvironment(from context: AppContext, theme: Theme) -> some View { public func withEnvironment(from context: AppContext, theme: Theme) -> some View {
environmentObject(theme) environmentObject(theme)
.environmentObject(context.connectionObserver)
.environmentObject(context.iapManager) .environmentObject(context.iapManager)
.environmentObject(context.profileManager)
.environmentObject(context.profileProcessor) .environmentObject(context.profileProcessor)
.environmentObject(context.providerManager) .environmentObject(context.providerManager)
.environmentObject(context.tunnel)
} }
public func withMockEnvironment() -> some View { public func withMockEnvironment() -> some View {

View File

@ -88,18 +88,12 @@ extension ProfileProcessor {
} }
} }
extension Tunnel { extension ExtendedTunnel {
public static var mock: Tunnel { public static var mock: ExtendedTunnel {
AppContext.mock.tunnel AppContext.mock.tunnel
} }
} }
extension ConnectionObserver {
public static var mock: ConnectionObserver {
AppContext.mock.connectionObserver
}
}
extension ProviderManager { extension ProviderManager {
public static var mock: ProviderManager { public static var mock: ProviderManager {
AppContext.mock.providerManager AppContext.mock.providerManager

View File

@ -30,7 +30,7 @@ import PassepartoutKit
public protocol AppCoordinatorConforming { public protocol AppCoordinatorConforming {
init( init(
profileManager: ProfileManager, profileManager: ProfileManager,
tunnel: Tunnel, tunnel: ExtendedTunnel,
registry: Registry registry: Registry
) )
} }

View File

@ -1,46 +0,0 @@
//
// TunnelContextProviding.swift
// Passepartout
//
// Created by Davide De Rosa on 9/5/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
import PassepartoutKit
public protocol TunnelContextProviding {
var connectionObserver: ConnectionObserver { get }
}
@MainActor
extension TunnelContextProviding {
public var tunnelConnectionStatus: TunnelStatus {
var status = connectionObserver.tunnel.status
if status == .active, let connectionStatus = connectionObserver.connectionStatus {
if connectionStatus == .connected {
status = .active
} else {
status = .activating
}
}
return status
}
}

View File

@ -30,7 +30,7 @@ import PassepartoutKit
public protocol TunnelInstallationProviding { public protocol TunnelInstallationProviding {
var profileManager: ProfileManager { get } var profileManager: ProfileManager { get }
var tunnel: Tunnel { get } var tunnel: ExtendedTunnel { get }
} }
@MainActor @MainActor

View File

@ -27,32 +27,34 @@ import Foundation
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
struct ConnectionStatusView: View, TunnelContextProviding, ThemeProviding { public struct ConnectionStatusView: View, ThemeProviding {
@EnvironmentObject @EnvironmentObject
var theme: Theme public var theme: Theme
@EnvironmentObject
var connectionObserver: ConnectionObserver
@ObservedObject @ObservedObject
var tunnel: Tunnel private var tunnel: ExtendedTunnel
var body: some View { public init(tunnel: ExtendedTunnel) {
self.tunnel = tunnel
}
public var body: some View {
Text(statusDescription) Text(statusDescription)
.foregroundStyle(tunnelStatusColor) .font(.headline)
.foregroundStyle(tunnel.statusColor(theme))
} }
} }
private extension ConnectionStatusView { private extension ConnectionStatusView {
var statusDescription: String { var statusDescription: String {
if let lastErrorCode = connectionObserver.lastErrorCode { if let lastErrorCode = tunnel.lastErrorCode {
return lastErrorCode.localizedDescription return lastErrorCode.localizedDescription
} }
let status = tunnelConnectionStatus let status = tunnel.connectionStatus
switch status { switch status {
case .active: case .active:
if let dataCount = connectionObserver.dataCount { if let dataCount = tunnel.dataCount {
let down = dataCount.received.descriptionAsDataUnit let down = dataCount.received.descriptionAsDataUnit
let up = dataCount.sent.descriptionAsDataUnit let up = dataCount.sent.descriptionAsDataUnit
return "\(down)\(up)" return "\(down)\(up)"
@ -75,7 +77,7 @@ private extension ConnectionStatusView {
#Preview("Connected") { #Preview("Connected") {
ConnectionStatusView(tunnel: .mock) ConnectionStatusView(tunnel: .mock)
.task { .task {
try? await Tunnel.mock.connect(with: .mock, processor: .mock) try? await ExtendedTunnel.mock.connect(with: .mock, processor: .mock)
} }
.frame(width: 100, height: 100) .frame(width: 100, height: 100)
.withMockEnvironment() .withMockEnvironment()
@ -94,7 +96,7 @@ private extension ConnectionStatusView {
} }
return ConnectionStatusView(tunnel: .mock) return ConnectionStatusView(tunnel: .mock)
.task { .task {
try? await Tunnel.mock.connect(with: profile, processor: .mock) try? await ExtendedTunnel.mock.connect(with: profile, processor: .mock)
} }
.frame(width: 100, height: 100) .frame(width: 100, height: 100)
.withMockEnvironment() .withMockEnvironment()

View File

@ -1,5 +1,5 @@
// //
// TunnelContextProviding+Theme.swift // ExtendedTunnel+Theme.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 9/6/24. // Created by Davide De Rosa on 9/6/24.
@ -26,11 +26,12 @@
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
@MainActor extension ExtendedTunnel {
extension TunnelContextProviding where Self: ThemeProviding {
var tunnelStatusColor: Color { @MainActor
if connectionObserver.lastErrorCode != nil { public func statusColor(_ theme: Theme) -> Color {
switch connectionObserver.tunnel.status { if lastErrorCode != nil {
switch status {
case .inactive: case .inactive:
return theme.inactiveColor return theme.inactiveColor
@ -38,7 +39,7 @@ extension TunnelContextProviding where Self: ThemeProviding {
return theme.errorColor return theme.errorColor
} }
} }
switch tunnelConnectionStatus { switch connectionStatus {
case .active: case .active:
return theme.activeColor return theme.activeColor

View File

@ -27,18 +27,15 @@ import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary import UtilsLibrary
struct TunnelToggleButton<Label>: View, TunnelContextProviding, ThemeProviding where Label: View { public struct TunnelToggleButton<Label>: View, ThemeProviding where Label: View {
enum Style { public enum Style {
case plain case plain
case color case color
} }
@EnvironmentObject @EnvironmentObject
var theme: Theme public var theme: Theme
@EnvironmentObject
var connectionObserver: ConnectionObserver
@EnvironmentObject @EnvironmentObject
private var iapManager: IAPManager private var iapManager: IAPManager
@ -46,25 +43,45 @@ struct TunnelToggleButton<Label>: View, TunnelContextProviding, ThemeProviding w
@EnvironmentObject @EnvironmentObject
private var profileProcessor: ProfileProcessor private var profileProcessor: ProfileProcessor
var style: Style = .plain private let style: Style
@ObservedObject @ObservedObject
var tunnel: Tunnel private var tunnel: ExtendedTunnel
let profile: Profile? private let profile: Profile?
@Binding @Binding
var nextProfileId: Profile.ID? private var nextProfileId: Profile.ID?
let interactiveManager: InteractiveManager private let interactiveManager: InteractiveManager
let errorHandler: ErrorHandler private let errorHandler: ErrorHandler
var onProviderEntityRequired: ((Profile) -> Void)? private let onProviderEntityRequired: ((Profile) -> Void)?
let label: (Bool) -> Label private let label: (Bool) -> Label
var body: some View { public init(
style: Style = .plain,
tunnel: ExtendedTunnel,
profile: Profile?,
nextProfileId: Binding<Profile.ID?>,
interactiveManager: InteractiveManager,
errorHandler: ErrorHandler,
onProviderEntityRequired: ((Profile) -> Void)? = nil,
label: @escaping (Bool) -> Label
) {
self.style = style
self.tunnel = tunnel
self.profile = profile
_nextProfileId = nextProfileId
self.interactiveManager = interactiveManager
self.errorHandler = errorHandler
self.onProviderEntityRequired = onProviderEntityRequired
self.label = label
}
public var body: some View {
Button(action: tryPerform) { Button(action: tryPerform) {
label(canConnect) label(canConnect)
} }
@ -92,7 +109,7 @@ private extension TunnelToggleButton {
return .primary return .primary
case .color: case .color:
return tunnelStatusColor return tunnel.statusColor(theme)
} }
} }
} }

View File

@ -39,7 +39,7 @@ extension Issue {
let purchasedProducts: Set<AppProduct> let purchasedProducts: Set<AppProduct>
let tunnel: Tunnel let tunnel: ExtendedTunnel
let urlForTunnelLog: URL let urlForTunnelLog: URL

View File

@ -31,7 +31,7 @@ import UtilsLibrary
struct InstalledProfileView: View, Routable { struct InstalledProfileView: View, Routable {
@EnvironmentObject @EnvironmentObject
var theme: Theme private var theme: Theme
let layout: ProfilesLayout let layout: ProfilesLayout
@ -39,7 +39,8 @@ struct InstalledProfileView: View, Routable {
let profile: Profile? let profile: Profile?
let tunnel: Tunnel @ObservedObject
var tunnel: ExtendedTunnel
let interactiveManager: InteractiveManager let interactiveManager: InteractiveManager

View File

@ -31,7 +31,7 @@ import UtilsLibrary
struct ProfileContextMenu: View, Routable { struct ProfileContextMenu: View, Routable {
let profileManager: ProfileManager let profileManager: ProfileManager
let tunnel: Tunnel let tunnel: ExtendedTunnel
let header: ProfileHeader let header: ProfileHeader

View File

@ -28,21 +28,18 @@ import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary import UtilsLibrary
struct ProfileRowView: View, Routable, TunnelContextProviding { struct ProfileRowView: View, Routable {
@EnvironmentObject @EnvironmentObject
private var theme: Theme private var theme: Theme
@EnvironmentObject
var connectionObserver: ConnectionObserver
let style: ProfileCardView.Style let style: ProfileCardView.Style
@ObservedObject @ObservedObject
var profileManager: ProfileManager var profileManager: ProfileManager
@ObservedObject @ObservedObject
var tunnel: Tunnel var tunnel: ExtendedTunnel
let header: ProfileHeader let header: ProfileHeader
@ -116,7 +113,7 @@ private extension ProfileRowView {
} }
var statusImage: Theme.ImageName { var statusImage: Theme.ImageName {
switch tunnelConnectionStatus { switch tunnel.connectionStatus {
case .active: case .active:
return .marked return .marked

View File

@ -33,7 +33,7 @@ struct TunnelRestartButton<Label>: View where Label: View {
private var profileProcessor: ProfileProcessor private var profileProcessor: ProfileProcessor
@ObservedObject @ObservedObject
var tunnel: Tunnel var tunnel: ExtendedTunnel
let profile: Profile? let profile: Profile?

View File

@ -35,7 +35,7 @@ struct AboutRouterView: View {
let profileManager: ProfileManager let profileManager: ProfileManager
let tunnel: Tunnel let tunnel: ExtendedTunnel
@State @State
var navigationRoute: NavigationRoute? var navigationRoute: NavigationRoute?
@ -63,7 +63,10 @@ extension AboutRouterView {
DonateView() DonateView()
case .diagnostics: case .diagnostics:
DiagnosticsView() DiagnosticsView(
profileManager: profileManager,
tunnel: tunnel
)
case .appDebugLog(let title): case .appDebugLog(let title):
DebugLogView.withApp(parameters: Constants.shared.log) DebugLogView.withApp(parameters: Constants.shared.log)

View File

@ -35,7 +35,7 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
private let profileManager: ProfileManager private let profileManager: ProfileManager
private let tunnel: Tunnel private let tunnel: ExtendedTunnel
private let registry: Registry private let registry: Registry
@ -53,7 +53,7 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
public init( public init(
profileManager: ProfileManager, profileManager: ProfileManager,
tunnel: Tunnel, tunnel: ExtendedTunnel,
registry: Registry registry: Registry
) { ) {
self.profileManager = profileManager self.profileManager = profileManager

View File

@ -28,12 +28,12 @@ import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary import UtilsLibrary
struct ProfileContainerView: View, Routable, TunnelInstallationProviding { struct ProfileContainerView: View, Routable {
let layout: ProfilesLayout let layout: ProfilesLayout
let profileManager: ProfileManager let profileManager: ProfileManager
let tunnel: Tunnel let tunnel: ExtendedTunnel
let registry: Registry let registry: Registry

View File

@ -37,7 +37,7 @@ struct ProfileGridView: View, Routable, TunnelInstallationProviding {
var profileManager: ProfileManager var profileManager: ProfileManager
@ObservedObject @ObservedObject
var tunnel: Tunnel var tunnel: ExtendedTunnel
let interactiveManager: InteractiveManager let interactiveManager: InteractiveManager

View File

@ -43,7 +43,7 @@ struct ProfileListView: View, Routable, TunnelInstallationProviding {
var profileManager: ProfileManager var profileManager: ProfileManager
@ObservedObject @ObservedObject
var tunnel: Tunnel var tunnel: ExtendedTunnel
let interactiveManager: InteractiveManager let interactiveManager: InteractiveManager

View File

@ -36,7 +36,7 @@ struct ProviderEntitySelector: View {
var profileManager: ProfileManager var profileManager: ProfileManager
@ObservedObject @ObservedObject
var tunnel: Tunnel var tunnel: ExtendedTunnel
let profile: Profile let profile: Profile

View File

@ -32,19 +32,22 @@ import SwiftUI
public struct AppMenu: View { public struct AppMenu: View {
@EnvironmentObject @ObservedObject
private var profileManager: ProfileManager private var profileManager: ProfileManager
@EnvironmentObject @ObservedObject
private var profileProcessor: ProfileProcessor private var profileProcessor: ProfileProcessor
@EnvironmentObject @ObservedObject
private var tunnel: Tunnel private var tunnel: ExtendedTunnel
@StateObject @StateObject
private var model = Model() private var model = Model()
public init() { public init(profileManager: ProfileManager, profileProcessor: ProfileProcessor, tunnel: ExtendedTunnel) {
self.profileManager = profileManager
self.profileProcessor = profileProcessor
self.tunnel = tunnel
} }
public var body: some View { public var body: some View {

View File

@ -28,17 +28,17 @@
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
public struct AppMenuImage: View, TunnelContextProviding { public struct AppMenuImage: View {
@ObservedObject @ObservedObject
public var connectionObserver: ConnectionObserver private var tunnel: ExtendedTunnel
public init(connectionObserver: ConnectionObserver) { public init(tunnel: ExtendedTunnel) {
self.connectionObserver = connectionObserver self.tunnel = tunnel
} }
public var body: some View { public var body: some View {
ThemeMenuImage(tunnelConnectionStatus.imageName) ThemeMenuImage(tunnel.connectionStatus.imageName)
} }
} }

View File

@ -35,7 +35,7 @@ extension DebugLogView {
} }
} }
static func withTunnel(_ tunnel: Tunnel, parameters: Constants.Log) -> DebugLogView { static func withTunnel(_ tunnel: ExtendedTunnel, parameters: Constants.Log) -> DebugLogView {
DebugLogView { DebugLogView {
await tunnel.currentLog(parameters: parameters) await tunnel.currentLog(parameters: parameters)
} }

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>. // along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
// //
import AppLibrary
import CommonLibrary import CommonLibrary
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
@ -41,15 +42,16 @@ struct DiagnosticsView: View {
@EnvironmentObject @EnvironmentObject
private var theme: Theme private var theme: Theme
@EnvironmentObject
private var connectionObserver: ConnectionObserver
@EnvironmentObject @EnvironmentObject
private var iapManager: IAPManager private var iapManager: IAPManager
@AppStorage(AppPreference.logsPrivateData.key, store: .appGroup) @AppStorage(AppPreference.logsPrivateData.key, store: .appGroup)
private var logsPrivateData = false private var logsPrivateData = false
let profileManager: ProfileManager
let tunnel: ExtendedTunnel
var availableTunnelLogs: () async -> [LogEntry] = { var availableTunnelLogs: () async -> [LogEntry] = {
await Task.detached { await Task.detached {
PassepartoutConfiguration.shared.availableLogs(at: BundleConfiguration.urlForTunnelLog) PassepartoutConfiguration.shared.availableLogs(at: BundleConfiguration.urlForTunnelLog)
@ -128,7 +130,7 @@ private extension DiagnosticsView {
} }
var openVPNSection: some View { var openVPNSection: some View {
connectionObserver.value(forKey: TunnelEnvironmentKeys.OpenVPN.serverConfiguration) tunnel.value(forKey: TunnelEnvironmentKeys.OpenVPN.serverConfiguration)
.map { cfg in .map { cfg in
Group { Group {
NavigationLink(Strings.Views.Diagnostics.Openvpn.Rows.serverConfiguration) { NavigationLink(Strings.Views.Diagnostics.Openvpn.Rows.serverConfiguration) {
@ -143,7 +145,8 @@ private extension DiagnosticsView {
var reportIssueSection: some View { var reportIssueSection: some View {
Section { Section {
ReportIssueButton( ReportIssueButton(
tunnel: connectionObserver.tunnel, profileManager: profileManager,
tunnel: tunnel,
title: Strings.Views.Diagnostics.ReportIssue.title, title: Strings.Views.Diagnostics.ReportIssue.title,
purchasedProducts: iapManager.purchasedProducts, purchasedProducts: iapManager.purchasedProducts,
isUnableToEmail: $isPresentingUnableToEmail isUnableToEmail: $isPresentingUnableToEmail
@ -190,7 +193,7 @@ private extension DiagnosticsView {
} }
#Preview { #Preview {
DiagnosticsView { DiagnosticsView(profileManager: .mock, tunnel: .mock) {
[ [
.init(date: Date(), url: URL(string: "http://one.com")!), .init(date: Date(), url: URL(string: "http://one.com")!),
.init(date: Date().addingTimeInterval(-60), url: URL(string: "http://two.com")!), .init(date: Date().addingTimeInterval(-60), url: URL(string: "http://two.com")!),

View File

@ -29,13 +29,13 @@ import SwiftUI
struct ReportIssueButton { struct ReportIssueButton {
@EnvironmentObject
private var profileManager: ProfileManager
@EnvironmentObject @EnvironmentObject
private var providerManager: ProviderManager private var providerManager: ProviderManager
let tunnel: Tunnel @ObservedObject
var profileManager: ProfileManager
let tunnel: ExtendedTunnel
let title: String let title: String

View File

@ -29,10 +29,10 @@ import SwiftUI
struct ProviderContentModifier<Entity, ProviderRows>: ViewModifier where Entity: ProviderEntity, Entity.Configuration: ProviderConfigurationIdentifiable & Codable, ProviderRows: View { struct ProviderContentModifier<Entity, ProviderRows>: ViewModifier where Entity: ProviderEntity, Entity.Configuration: ProviderConfigurationIdentifiable & Codable, ProviderRows: View {
@EnvironmentObject @EnvironmentObject
private var providerManager: ProviderManager private var iapManager: IAPManager
@EnvironmentObject @EnvironmentObject
private var iapManager: IAPManager private var providerManager: ProviderManager
let apis: [APIMapper] let apis: [APIMapper]

View File

@ -32,11 +32,11 @@ import SwiftUI
public struct AppCoordinator: View, AppCoordinatorConforming { public struct AppCoordinator: View, AppCoordinatorConforming {
private let profileManager: ProfileManager private let profileManager: ProfileManager
private let tunnel: Tunnel private let tunnel: ExtendedTunnel
private let registry: Registry private let registry: Registry
public init(profileManager: ProfileManager, tunnel: Tunnel, registry: Registry) { public init(profileManager: ProfileManager, tunnel: ExtendedTunnel, registry: Registry) {
self.profileManager = profileManager self.profileManager = profileManager
self.tunnel = tunnel self.tunnel = tunnel
self.registry = registry self.registry = registry

View File

@ -1,5 +1,5 @@
// //
// ConnectionObserverTests.swift // ExtendedTunnelTests.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 9/12/24. // Created by Davide De Rosa on 9/12/24.
@ -28,15 +28,15 @@ import Foundation
import PassepartoutKit import PassepartoutKit
import XCTest import XCTest
final class ConnectionObserverTests: XCTestCase { final class ExtendedTunnelTests: XCTestCase {
} }
@MainActor @MainActor
extension ConnectionObserverTests { extension ExtendedTunnelTests {
func test_givenTunnel_whenDisconnectWithError_thenPublishesLastErrorCode() async throws { func test_givenTunnel_whenDisconnectWithError_thenPublishesLastErrorCode() async throws {
let env = InMemoryEnvironment() let env = InMemoryEnvironment()
let tunnel = Tunnel(strategy: FakeTunnelStrategy(environment: env)) let tunnel = Tunnel(strategy: FakeTunnelStrategy(environment: env))
let sut = ConnectionObserver(tunnel: tunnel, environment: env, interval: 0.1) let sut = ExtendedTunnel(tunnel: tunnel, environment: env, interval: 0.1)
sut.observeObjects() sut.observeObjects()
let profile = try Profile.Builder().tryBuild() let profile = try Profile.Builder().tryBuild()
@ -51,7 +51,7 @@ extension ConnectionObserverTests {
func test_givenTunnel_whenConnect_thenPublishesDataCount() async throws { func test_givenTunnel_whenConnect_thenPublishesDataCount() async throws {
let env = InMemoryEnvironment() let env = InMemoryEnvironment()
let tunnel = Tunnel(strategy: FakeTunnelStrategy(environment: env)) let tunnel = Tunnel(strategy: FakeTunnelStrategy(environment: env))
let sut = ConnectionObserver(tunnel: tunnel, environment: env, interval: 0.1) let sut = ExtendedTunnel(tunnel: tunnel, environment: env, interval: 0.1)
sut.observeObjects() sut.observeObjects()
let profile = try Profile.Builder().tryBuild() let profile = try Profile.Builder().tryBuild()