diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index de7412bbba..c7f43fd49d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14568,7 +14568,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - branch = "anh/netp/location-selection"; + branch = "anh/netp/screen-improvements"; kind = branch; }; }; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c07ef413be..dcc3605227 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "branch" : "anh/netp/location-selection", - "revision" : "dc6725de9e7019ef7e2b0bb7438b9d98010808f8" + "branch" : "anh/netp/screen-improvements", + "revision" : "3b354a70cdfff5d46d2c8f525748dee63a36ad88" } }, { diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index e261efdf12..a0f4956d8c 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -145,6 +145,8 @@ final class URLEventHandler { WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) case AppLaunchCommand.shareFeedback.launchURL: WindowControllersManager.shared.showShareFeedbackModal() + case AppLaunchCommand.justOpen.launchURL: + WindowControllersManager.shared.showMainWindow() case AppLaunchCommand.showVPNLocations.launchURL: WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) WindowControllersManager.shared.showLocationPickerSheet() diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 317a7c5178..4dd17342ea 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -42,8 +42,8 @@ extension UserText { static let networkProtectionInviteSuccessMessage = "DuckDuckGo's VPN secures all of your device's Internet traffic anytime, anywhere." // MARK: - Navigation Bar Status View - // "network.protection.navbar.status.view.share.feedback" - Menu item for 'Send VPN Feedback' in the VPN status view that's shown in the navigation bar - static let networkProtectionNavBarStatusViewShareFeedback = "Send VPN Feedback…" + // "network.protection.navbar.status.view.share.feedback" - Menu item for 'Share VPN Feedback' in the VPN status view that's shown in the navigation bar + static let networkProtectionNavBarStatusViewShareFeedback = "Share VPN Feedback…" // "network.protection.status.menu.vpn.settings" - The status menu 'VPN Settings' menu item static let networkProtectionNavBarStatusMenuVPNSettings = "VPN Settings…" // "network.protection.status.menu.faq" - The status menu 'FAQ' menu item @@ -299,7 +299,7 @@ extension UserText { // "vpn.location.description.nearest" - Nearest city setting description static let vpnLocationNearest = "Nearest" // "vpn.location.description.nearest.available" - Nearest available location setting description - static let vpnLocationNearestAvailable = "Nearest available" + static let vpnLocationNearestAvailable = "Nearest Location" // "vpn.location.nearest.available.title" - Subtitle underneath the nearest available vpn location preference text. static let vpnLocationNearestAvailableSubtitle = "Automatically connect to the nearest server we can find." diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 04b5810087..f2b57e1c54 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -93,7 +93,8 @@ final class MainViewController: NSViewController { serverInfoObserver: ipcClient.ipcServerInfoObserver, connectionErrorObserver: ipcClient.ipcConnectionErrorObserver, connectivityIssuesObserver: connectivityIssuesObserver, - controllerErrorMessageObserver: controllerErrorMessageObserver + controllerErrorMessageObserver: controllerErrorMessageObserver, + dataVolumeObserver: ipcClient.ipcDataVolumeObserver ) }() diff --git a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift index 0af23bdcd5..0007a3e54a 100644 --- a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift +++ b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift @@ -63,6 +63,12 @@ final class IPCClientMock: NetworkProtectionIPCClient { } var ipcControllerErrorMessageObserver: any NetworkProtection.ControllerErrorMesssageObserver = ControllerErrorMesssageObserverMock() + final class DataVolumeObserverMock: NetworkProtection.DataVolumeObserver { + var publisher: AnyPublisher = PassthroughSubject().eraseToAnyPublisher() + var recentValue: DataVolume = .init() + } + var ipcDataVolumeObserver: any NetworkProtection.DataVolumeObserver = DataVolumeObserverMock() + func start() {} func stop() {} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index fcf40ff007..27bacb11ad 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -32,6 +32,7 @@ protocol NetworkProtectionIPCClient { var ipcStatusObserver: ConnectionStatusObserver { get } var ipcServerInfoObserver: ConnectionServerInfoObserver { get } var ipcConnectionErrorObserver: ConnectionErrorObserver { get } + var ipcDataVolumeObserver: DataVolumeObserver { get } func start() func stop() @@ -41,6 +42,7 @@ extension TunnelControllerIPCClient: NetworkProtectionIPCClient { public var ipcStatusObserver: any NetworkProtection.ConnectionStatusObserver { connectionStatusObserver } public var ipcServerInfoObserver: any NetworkProtection.ConnectionServerInfoObserver { serverInfoObserver } public var ipcConnectionErrorObserver: any NetworkProtection.ConnectionErrorObserver { connectionErrorObserver } + public var ipcDataVolumeObserver: any NetworkProtection.DataVolumeObserver { dataVolumeObserver } } final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { @@ -69,7 +71,8 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { serverInfoObserver: ipcClient.ipcServerInfoObserver, connectionErrorObserver: ipcClient.ipcConnectionErrorObserver, connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), - controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() + controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), + dataVolumeObserver: ipcClient.ipcDataVolumeObserver ) let onboardingStatusPublisher = UserDefaults.netP.networkProtectionOnboardingStatusPublisher diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index c24d64bf6e..b4e463df17 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -121,7 +121,8 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { serverInfoObserver: ipcClient.serverInfoObserver, connectionErrorObserver: ipcClient.connectionErrorObserver, connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), - controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() + controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), + dataVolumeObserver: ipcClient.dataVolumeObserver ) // Force refresh just in case. A refresh is requested when the IPC client is created, but distributed notifications don't guarantee delivery diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index ae37e64c41..10758ce56d 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -237,6 +237,13 @@ extension WindowControllersManager { } } + func showMainWindow() { + guard WindowControllersManager.shared.lastKeyMainWindowController == nil else { return } + let tabCollection = TabCollection(tabs: []) + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) + _ = WindowsManager.openNewWindow(with: tabCollectionViewModel) + } + func showLocationPickerSheet() { let locationsViewController = VPNLocationsHostingViewController() let locationsWindowController = locationsViewController.wrappedInWindowController() diff --git a/DuckDuckGoDBPBackgroundAgent/UserText.swift b/DuckDuckGoDBPBackgroundAgent/UserText.swift index 5b460321e5..608cab9e14 100644 --- a/DuckDuckGoDBPBackgroundAgent/UserText.swift +++ b/DuckDuckGoDBPBackgroundAgent/UserText.swift @@ -21,6 +21,6 @@ import Foundation final class UserText { // MARK: - Status Menu - static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Send VPN Feedback...", comment: "The status menu 'Send VPN Feedback' menu item") - static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.open.duckduckgo", value: "Open DuckDuckGo...", comment: "The status menu 'Open DuckDuckGo' menu item") + static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share VPN Feedback…", comment: "The status menu 'Share VPN Feedback' menu item") + static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.open.duckduckgo", value: "Open DuckDuckGo…", comment: "The status menu 'Open DuckDuckGo' menu item") } diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 124ce85aec..034d768892 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -187,12 +187,18 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { platformNotificationCenter: NSWorkspace.shared.notificationCenter, platformDidWakeNotification: NSWorkspace.didWakeNotification) + let dataVolumeObserver = DataVolumeObserverThroughSession( + tunnelSessionProvider: tunnelController, + platformNotificationCenter: NSWorkspace.shared.notificationCenter, + platformDidWakeNotification: NSWorkspace.didWakeNotification) + return DefaultNetworkProtectionStatusReporter( statusObserver: statusObserver, serverInfoObserver: serverInfoObserver, connectionErrorObserver: errorObserver, connectivityIssuesObserver: DisabledConnectivityIssueObserver(), - controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() + controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications(), + dataVolumeObserver: dataVolumeObserver ) }() @@ -245,12 +251,12 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuFAQ, action: { [weak self] in await self?.appLauncher.launchApp(withCommand: .showFAQ) }), + StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuShareFeedback, action: { [weak self] in + await self?.appLauncher.launchApp(withCommand: .shareFeedback) + }), StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuOpenDuckDuckGo, action: { [weak self] in await self?.appLauncher.launchApp(withCommand: .justOpen) }), - StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuShareFeedback, action: { [weak self] in - await self?.appLauncher.launchApp(withCommand: .shareFeedback) - }) ] }, agentLoginItem: nil, diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index c8a9ce456d..ad0faba0cb 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -50,6 +50,7 @@ final class TunnelControllerIPCService { subscribeToErrorChanges() subscribeToStatusUpdates() subscribeToServerChanges() + subscribeToDataVolumeUpdates() server.serverDelegate = self } @@ -84,6 +85,15 @@ final class TunnelControllerIPCService { } .store(in: &cancellables) } + + private func subscribeToDataVolumeUpdates() { + statusReporter.dataVolumeObserver.publisher + .subscribe(on: DispatchQueue.main) + .sink { [weak self] dataVolume in + self?.server.dataVolumeUpdated(dataVolume) + } + .store(in: &cancellables) + } } // MARK: - Requests from the client diff --git a/DuckDuckGoVPN/UserText.swift b/DuckDuckGoVPN/UserText.swift index 57ea5e3c3b..4c64c664eb 100644 --- a/DuckDuckGoVPN/UserText.swift +++ b/DuckDuckGoVPN/UserText.swift @@ -24,5 +24,5 @@ final class UserText { static let networkProtectionStatusMenuVPNSettings = NSLocalizedString("network.protection.status.menu.vpn.settings", value: "VPN Settings…", comment: "The status menu 'VPN Settings' menu item") static let networkProtectionStatusMenuFAQ = NSLocalizedString("network.protection.status.menu.faq", value: "Frequently Asked Questions…", comment: "The status menu 'FAQ' menu item") static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.vpn.open-duckduckgo", value: "Open DuckDuckGo…", comment: "The status menu 'Open DuckDuckGo' menu item") - static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Send VPN Feedback…", comment: "The status menu 'Send VPN Feedback' menu item") + static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share VPN Feedback…", comment: "The status menu 'Share VPN Feedback' menu item") } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/DataVolumeObserverThroughIPC.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/DataVolumeObserverThroughIPC.swift new file mode 100644 index 0000000000..ee605da45a --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/Observers/DataVolumeObserverThroughIPC.swift @@ -0,0 +1,40 @@ +// +// DataVolumeObserverThroughIPC.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 Combine +import Foundation +import NetworkProtection + +public final class DataVolumeObserverThroughIPC: DataVolumeObserver { + + private let subject = CurrentValueSubject(.init()) + + // MARK: - DataVolumeObserver + + public lazy var publisher = subject.eraseToAnyPublisher() + + public var recentValue: DataVolume { + subject.value + } + + // MARK: - Publishing Updates + + func publish(_ dataVolume: DataVolume) { + subject.send(dataVolume) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift index 769939f65e..695218ab96 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift @@ -26,6 +26,7 @@ public protocol IPCClientInterface: AnyObject { func errorChanged(_ error: String?) func serverInfoChanged(_ serverInfo: NetworkProtectionStatusServerInfo) func statusChanged(_ status: ConnectionStatus) + func dataVolumeUpdated(_ dataVolume: DataVolume) } /// This is the XPC interface with parameters that can be packed properly @@ -34,6 +35,7 @@ protocol XPCClientInterface { func errorChanged(error: String?) func serverInfoChanged(payload: Data) func statusChanged(payload: Data) + func dataVolumeUpdated(payload: Data) } public final class TunnelControllerIPCClient { @@ -47,6 +49,7 @@ public final class TunnelControllerIPCClient { public var serverInfoObserver = ConnectionServerInfoObserverThroughIPC() public var connectionErrorObserver = ConnectionErrorObserverThroughIPC() public var connectionStatusObserver = ConnectionStatusObserverThroughIPC() + public var dataVolumeObserver = DataVolumeObserverThroughIPC() /// The delegate. /// @@ -65,7 +68,8 @@ public final class TunnelControllerIPCClient { clientDelegate: self.clientDelegate, serverInfoObserver: self.serverInfoObserver, connectionErrorObserver: self.connectionErrorObserver, - connectionStatusObserver: self.connectionStatusObserver + connectionStatusObserver: self.connectionStatusObserver, + dataVolumeObserver: self.dataVolumeObserver ) xpc = XPCClient( @@ -97,15 +101,18 @@ private final class TunnelControllerXPCClientDelegate: XPCClientInterface { let serverInfoObserver: ConnectionServerInfoObserverThroughIPC let connectionErrorObserver: ConnectionErrorObserverThroughIPC let connectionStatusObserver: ConnectionStatusObserverThroughIPC + let dataVolumeObserver: DataVolumeObserverThroughIPC init(clientDelegate: IPCClientInterface?, serverInfoObserver: ConnectionServerInfoObserverThroughIPC, connectionErrorObserver: ConnectionErrorObserverThroughIPC, - connectionStatusObserver: ConnectionStatusObserverThroughIPC) { + connectionStatusObserver: ConnectionStatusObserverThroughIPC, + dataVolumeObserver: DataVolumeObserverThroughIPC) { self.clientDelegate = clientDelegate self.serverInfoObserver = serverInfoObserver self.connectionErrorObserver = connectionErrorObserver self.connectionStatusObserver = connectionStatusObserver + self.dataVolumeObserver = dataVolumeObserver } func errorChanged(error: String?) { @@ -130,6 +137,15 @@ private final class TunnelControllerXPCClientDelegate: XPCClientInterface { connectionStatusObserver.publish(status) clientDelegate?.statusChanged(status) } + + func dataVolumeUpdated(payload: Data) { + guard let dataVolume = try? JSONDecoder().decode(DataVolume.self, from: payload) else { + return + } + + dataVolumeObserver.publish(dataVolume) + clientDelegate?.dataVolumeUpdated(dataVolume) + } } // MARK: - Outgoing communication to the server diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift index 3fe62c74f3..13ee59a0d3 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift @@ -135,6 +135,20 @@ extension TunnelControllerIPCServer: IPCClientInterface { client.statusChanged(payload: payload) } } + + public func dataVolumeUpdated(_ dataVolume: DataVolume) { + let payload: Data + + do { + payload = try JSONEncoder().encode(dataVolume) + } catch { + return + } + + xpc.forEachClient { client in + client.dataVolumeUpdated(payload: payload) + } + } } // MARK: - Incoming communication from a client diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift index 6e02224576..a8288b7dbd 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionAsset.swift @@ -19,12 +19,12 @@ import Foundation public enum NetworkProtectionAsset: String, CaseIterable { - case ipAddressIcon = "IP-16" - case serverLocationIcon = "Server-Location-16" case vpnDisabledImage = "VPNDisabled" case vpnEnabledImage = "VPN" case vpnIcon = "VPN-16" case nearestAvailable = "VPNLocation" + case dataReceived = "VPNDownload" + case dataSent = "VPNUpload" // Apple Icons case appleVaultIcon = "apple-vault-icon" diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionColor.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionColor.swift index 010085527a..2145e2da36 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionColor.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionColor.swift @@ -31,6 +31,7 @@ extension Color { /// enum NetworkProtectionColor: String { case defaultText = "TextColor" + case secondaryText = "SecondaryColor" case linkColor = "LinkBlueColor" case onboardingButtonBackgroundColor = "OnboardingButtonBackgroundColor" #if swift(<5.9) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json new file mode 100644 index 0000000000..8f7e96555c --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Colors/SecondaryColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.600", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.850", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/Contents.json deleted file mode 100644 index 657dbb1f2c..0000000000 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "IP-16.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/IP-16.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/IP-16.pdf deleted file mode 100644 index f68365bd0a..0000000000 Binary files a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/IP-16.imageset/IP-16.pdf and /dev/null differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Contents.json deleted file mode 100644 index e020cd9994..0000000000 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "Server-Location-16.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Server-Location-16.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Server-Location-16.pdf deleted file mode 100644 index b1f213c7b9..0000000000 Binary files a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/Server-Location-16.imageset/Server-Location-16.pdf and /dev/null differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json index c9df096cfc..e030aab433 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/Contents.json @@ -10,6 +10,6 @@ "version" : 1 }, "properties" : { - "template-rendering-intent" : "template" + "template-rendering-intent" : "original" } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/vpn-download.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/vpn-download.pdf index 55844a6647..d5288602de 100644 Binary files a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/vpn-download.pdf and b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNDownload.imageset/vpn-download.pdf differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/Contents.json index bd59670506..09ed1f69f3 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/Contents.json @@ -8,8 +8,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/VPNLocation.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/VPNLocation.pdf index 2cfa6eb89d..ddcabe468b 100644 Binary files a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/VPNLocation.pdf and b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNLocation.imageset/VPNLocation.pdf differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json index 4efc61b832..c7fec6a1a8 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/Contents.json @@ -10,6 +10,6 @@ "version" : 1 }, "properties" : { - "template-rendering-intent" : "template" + "template-rendering-intent" : "original" } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/vpn-upload.pdf b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/vpn-upload.pdf index 97a505ff33..4cc3e7eced 100644 Binary files a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/vpn-upload.pdf and b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Resources/Assets.xcassets/Icons/VPNUpload.imageset/vpn-upload.pdf differ diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift index 96244b1539..c4ede6adeb 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerView.swift @@ -28,6 +28,10 @@ fileprivate extension Font { .system(size: 13, weight: .regular, design: .default) } + static var dataVolume: Font { + .system(size: 13, weight: .regular, design: .default) + } + static var location: Font { .system(size: 13, weight: .regular, design: .default) } @@ -64,13 +68,12 @@ private enum Opacity { colorScheme == .light ? Double(0.6) : Double(0.5) } - static func location(colorScheme: ColorScheme) -> Double { + static func dataVolume(colorScheme: ColorScheme) -> Double { colorScheme == .light ? Double(0.6) : Double(0.5) } static let content = Double(0.58) static let label = Double(0.9) - static let description = Double(0.9) static let link = Double(1) static func sectionHeader(colorScheme: ColorScheme) -> Double { @@ -93,6 +96,12 @@ fileprivate extension View { .foregroundColor(Color(.defaultText)) } + func applyDataVolumeAttributes(colorScheme: ColorScheme) -> some View { + opacity(Opacity.dataVolume(colorScheme: colorScheme)) + .font(.NetworkProtection.dataVolume) + .foregroundColor(Color(.defaultText)) + } + func applyLocationAttributes() -> some View { font(.NetworkProtection.location) } @@ -103,10 +112,9 @@ fileprivate extension View { .foregroundColor(Color(.defaultText)) } - func applyDescriptionAttributes(colorScheme: ColorScheme) -> some View { - opacity(Opacity.description) - .font(.NetworkProtection.description) - .foregroundColor(Color(.defaultText)) + func applyDescriptionAttributes() -> some View { + font(.NetworkProtection.description) + .foregroundColor(Color(.secondaryText)) } func applyLabelAttributes(colorScheme: ColorScheme) -> some View { @@ -190,7 +198,7 @@ public struct TunnelControllerView: View { Text(model.isToggleOn.wrappedValue ? UserText.networkProtectionStatusHeaderMessageOn : UserText.networkProtectionStatusHeaderMessageOff) .multilineText() .multilineTextAlignment(.center) - .applyDescriptionAttributes(colorScheme: colorScheme) + .applyDescriptionAttributes() .fixedSize(horizontal: false, vertical: true) .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) } @@ -208,10 +216,6 @@ public struct TunnelControllerView: View { @ViewBuilder private func headerAnimationView(_ animationName: String) -> some View { LottieView(animation: .named(animationName)) - .configure { animationView in - animationView.contentMode = .scaleAspectFit - animationView.clipsToBounds = true - } .playing(withIntro: .init( skipIntro: model.isVPNEnabled && !model.isToggleDisabled, introStartFrame: 0, @@ -248,8 +252,21 @@ public struct TunnelControllerView: View { .background(Color(hex: "B2B2B2").opacity(0.3)) .clipShape(Circle()) } else if model.wantsNearestLocation { - Image(NetworkProtectionAsset.nearestAvailable) - .frame(width: 26, height: 26) + ZStack { + Circle() + .fill(Color(hex: "B2B2B2").opacity(0.3)) + .frame(width: 26, height: 26) + if isHovered { + Image(NetworkProtectionAsset.nearestAvailable) + .renderingMode(.template) + .foregroundColor(.white) + .frame(width: 16, height: 16) + } else { + Image(NetworkProtectionAsset.nearestAvailable) + .renderingMode(colorScheme == .light ? .original : .template) + .frame(width: 16, height: 16) + } + } } if #available(macOS 12, *) { if isHovered { @@ -257,7 +274,7 @@ public struct TunnelControllerView: View { .applyLocationAttributes() .foregroundColor(.white) } else { - Text(model.formattedLocation) + Text(model.formattedLocation(colorScheme: colorScheme)) .applyLocationAttributes() } } else { @@ -280,10 +297,11 @@ public struct TunnelControllerView: View { .applySectionHeaderAttributes(colorScheme: colorScheme) .padding(EdgeInsets(top: 6, leading: 9, bottom: 6, trailing: 9)) - connectionStatusRow(icon: .ipAddressIcon, - title: UserText.networkProtectionStatusViewIPAddress, + connectionStatusRow(title: UserText.networkProtectionStatusViewIPAddress, details: model.serverAddress) + dataVolumeRow(title: UserText.vpnDataVolume, dataVolume: model.formattedDataVolume) + dividerRow() } } @@ -322,11 +340,8 @@ public struct TunnelControllerView: View { .padding(EdgeInsets(top: 3, leading: 9, bottom: 3, trailing: 9)) } - private func connectionStatusRow(icon: NetworkProtectionAsset, title: String, details: String) -> some View { + private func connectionStatusRow(title: String, details: String) -> some View { HStack(spacing: 0) { - Image(icon) - .padding([.trailing], 8) - Text(title) .applyLabelAttributes(colorScheme: colorScheme) .fixedSize() @@ -338,6 +353,32 @@ public struct TunnelControllerView: View { .applyConnectionStatusDetailAttributes(colorScheme: colorScheme) .fixedSize() } + .padding(EdgeInsets(top: 6, leading: 10, bottom: 0, trailing: 9)) + } + + private func dataVolumeRow(title: String, dataVolume: TunnelControllerViewModel.FormattedDataVolume) -> some View { + HStack(spacing: 0) { + Text(title) + .applyLabelAttributes(colorScheme: colorScheme) + .fixedSize() + + Spacer(minLength: 2) + + Group { + Image(NetworkProtectionAsset.dataReceived) + .renderingMode(colorScheme == .light ? .original : .template) + .frame(width: 12, height: 12) + Text(dataVolume.dataReceived) + .applyDataVolumeAttributes(colorScheme: colorScheme) + Image(NetworkProtectionAsset.dataSent) + .renderingMode(colorScheme == .light ? .original : .template) + .frame(width: 12, height: 12) + .padding(.leading, 4) + Text(dataVolume.dataSent) + .applyDataVolumeAttributes(colorScheme: colorScheme) + } + .fixedSize() + } .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 9)) } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift index dfd96cd277..e28b2aed75 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/TunnelControllerView/TunnelControllerViewModel.swift @@ -23,6 +23,10 @@ import SwiftUI @MainActor public final class TunnelControllerViewModel: ObservableObject { + public struct FormattedDataVolume: Equatable { + public let dataSent: String + public let dataReceived: String + } /// The NetP service. /// @@ -56,6 +60,13 @@ public final class TunnelControllerViewModel: ObservableObject { private let locationFormatter: VPNLocationFormatting + private static let byteCountFormatter: ByteCountFormatter = { + let formatter = ByteCountFormatter() + formatter.allowsNonnumericFormatting = false + formatter.allowedUnits = [.useKB, .useMB, .useGB] + return formatter + }() + private let appLauncher: AppLaunching // MARK: - Misc @@ -70,6 +81,7 @@ public final class TunnelControllerViewModel: ObservableObject { private static let statusDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.statusDispatchQueue", qos: .userInteractive) private static let connectivityIssuesDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.connectivityIssuesDispatchQueue", qos: .userInteractive) private static let serverInfoDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.serverInfoDispatchQueue", qos: .userInteractive) + private static let dataVolumeDispatchQueue = DispatchQueue(label: "com.duckduckgo.NetworkProtectionStatusView.dataVolumeDispatchQueue", qos: .userInteractive) // MARK: - Initialization & Deinitialization @@ -90,6 +102,7 @@ public final class TunnelControllerViewModel: ObservableObject { self.appLauncher = appLauncher connectionStatus = statusReporter.statusObserver.recentValue + formattedDataVolume = statusReporter.dataVolumeObserver.recentValue.formatted(using: Self.byteCountFormatter) internalServerAddress = statusReporter.serverInfoObserver.recentValue.serverAddress internalServerAttributes = statusReporter.serverInfoObserver.recentValue.serverLocation internalServerLocation = internalServerAttributes?.serverLocation @@ -100,6 +113,7 @@ public final class TunnelControllerViewModel: ObservableObject { subscribeToOnboardingStatusChanges() subscribeToStatusChanges() subscribeToServerInfoChanges() + subscribeToDataVolumeUpdates() } deinit { @@ -156,6 +170,15 @@ public final class TunnelControllerViewModel: ObservableObject { .store(in: &cancellables) } + private func subscribeToDataVolumeUpdates() { + statusReporter.dataVolumeObserver.publisher + .subscribe(on: Self.dataVolumeDispatchQueue) + .map { $0.formatted(using: Self.byteCountFormatter) } + .receive(on: DispatchQueue.main) + .assign(to: \.formattedDataVolume, onWeaklyHeld: self) + .store(in: &cancellables) + } + // MARK: - ON/OFF Toggle private func startTimer() { @@ -444,6 +467,9 @@ public final class TunnelControllerViewModel: ObservableObject { @Published private var internalServerAttributes: NetworkProtectionServerInfo.ServerAttributes? + @Published + var formattedDataVolume: FormattedDataVolume + var wantsNearestLocation: Bool { guard case .nearest = vpnSettings.selectedLocation else { return false } return true @@ -460,11 +486,12 @@ public final class TunnelControllerViewModel: ObservableObject { } @available(macOS 12, *) - var formattedLocation: AttributedString { - locationFormatter.string(from: internalServerLocation, - preferredLocation: vpnSettings.selectedLocation, - locationTextColor: Color(.defaultText), - preferredLocationTextColor: Color(.defaultText).opacity(0.6)) + func formattedLocation(colorScheme: ColorScheme) -> AttributedString { + let opacity = colorScheme == .light ? Double(0.6) : Double(0.5) + return locationFormatter.string(from: internalServerLocation, + preferredLocation: vpnSettings.selectedLocation, + locationTextColor: Color(.defaultText), + preferredLocationTextColor: Color(.defaultText).opacity(opacity)) } // MARK: - Toggling VPN @@ -508,3 +535,10 @@ public final class TunnelControllerViewModel: ObservableObject { } } } + +extension DataVolume { + func formatted(using formatter: ByteCountFormatter) -> TunnelControllerViewModel.FormattedDataVolume { + .init(dataSent: formatter.string(fromByteCount: bytesSent), + dataReceived: formatter.string(fromByteCount: bytesReceived)) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift index 83d08e3ceb..b49d24d5b4 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionAssetTests.swift @@ -28,12 +28,12 @@ final class NetworkProtectionAssetTests: XCTestCase { /// func testAssetEnumValuesAreUnchanged() { let assetsAndExpectedRawValues: [NetworkProtectionAsset: String] = [ - .ipAddressIcon: "IP-16", - .serverLocationIcon: "Server-Location-16", .vpnDisabledImage: "VPNDisabled", .vpnEnabledImage: "VPN", .vpnIcon: "VPN-16", .nearestAvailable: "VPNLocation", + .dataReceived: "VPNDownload", + .dataSent: "VPNUpload", .appleVaultIcon: "apple-vault-icon", .appleVPNIcon: "apple-vpn-icon", .appleSystemSettingsIcon: "apple-system-settings-icon", diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift index 884dc3d002..e7785da3f4 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/TunnelControllerViewModelTests.swift @@ -36,12 +36,14 @@ final class TunnelControllerViewModelTests: XCTestCase { let connectionErrorObserver: ConnectionErrorObserver let connectivityIssuesObserver: ConnectivityIssueObserver let controllerErrorMessageObserver: ControllerErrorMesssageObserver + let dataVolumeObserver: DataVolumeObserver init(status: ConnectionStatus, isHavingConnectivityIssues: Bool = false, serverInfo: NetworkProtectionStatusServerInfo = MockStatusReporter.defaultServerInfo, tunnelErrorMessage: String? = nil, - controllerErrorMessage: String? = nil) { + controllerErrorMessage: String? = nil, + dataVolume: DataVolume = .init()) { let mockStatusObserver = MockConnectionStatusObserver() mockStatusObserver.subject.send(status) @@ -62,6 +64,10 @@ final class TunnelControllerViewModelTests: XCTestCase { let mockControllerErrorMessageObserver = MockControllerErrorMesssageObserver() mockControllerErrorMessageObserver.subject.send(controllerErrorMessage) controllerErrorMessageObserver = mockControllerErrorMessageObserver + + let mockDataVolumeObserver = MockDataVolumeObserver() + mockDataVolumeObserver.subject.send(dataVolume) + dataVolumeObserver = mockDataVolumeObserver } func forceRefresh() { @@ -189,6 +195,24 @@ final class TunnelControllerViewModelTests: XCTestCase { XCTAssertFalse(model.showServerDetails) } + /// We expect the model to properly reflect the data volume. + /// + @MainActor + func testProperlyReflectsDataVolume() async throws { + let controller = MockTunnelController() + let statusReporter = MockStatusReporter(status: .connected(connectedDate: Date()), + dataVolume: .init(bytesSent: 512000, bytesReceived: 1024000)) + let model = TunnelControllerViewModel( + controller: controller, + onboardingStatusPublisher: Just(OnboardingStatus.completed).eraseToAnyPublisher(), + statusReporter: statusReporter, + vpnSettings: .init(defaults: .standard), + locationFormatter: MockVPNLocationFormatter(), + appLauncher: MockAppLauncher()) + + XCTAssertEqual(model.formattedDataVolume, .init(dataSent: "512 KB", dataReceived: "1 MB")) + } + /// We expect that setting the model's `isRunning` to `true`, will start the VPN. /// @MainActor diff --git a/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift b/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift index 0c52440181..56190708be 100644 --- a/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift +++ b/UnitTests/NetworkProtection/DefaultVPNLocationFormatterTests.swift @@ -39,7 +39,7 @@ final class DefaultVPNLocationFormatterTests: XCTestCase { XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: preferredLocation), "🇺🇸") XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: otherPreferredLocation), "🇺🇸") - XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest available") + XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest Location") XCTAssertEqual(formatter.string(from: nil, preferredLocation: preferredLocation), "United States") XCTAssertEqual(formatter.string(from: nil, preferredLocation: otherPreferredLocation), "United Kingdom") XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: .nearest), "Lafayette, United States (Nearest)") @@ -69,7 +69,7 @@ final class DefaultVPNLocationFormatterTests: XCTestCase { XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: preferredLocation), "🇨🇦") XCTAssertEqual(formatter.emoji(for: server.country, preferredLocation: otherPreferredLocation), "🇨🇦") - XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest available") + XCTAssertEqual(formatter.string(from: nil, preferredLocation: .nearest), "Nearest Location") XCTAssertEqual(formatter.string(from: nil, preferredLocation: preferredLocation), "Canada") XCTAssertEqual(formatter.string(from: nil, preferredLocation: otherPreferredLocation), "United Kingdom") XCTAssertEqual(formatter.string(from: server.serverLocation, preferredLocation: .nearest), "Toronto, Canada (Nearest)")