Skip to content

Commit

Permalink
NetP Geoswitching Design Review feedback (#2206)
Browse files Browse the repository at this point in the history
  • Loading branch information
graeme authored Nov 29, 2023
1 parent 59dbb74 commit 82c8517
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 33 deletions.
4 changes: 4 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,7 @@
EE0153EF2A70021E002A8B26 /* NetworkProtectionInviteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153EE2A70021E002A8B26 /* NetworkProtectionInviteView.swift */; };
EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE01EB3F2AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift */; };
EE01EB432AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */; };
EE0798C52B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */; };
EE276BEA2A77F823009167B6 /* NetworkProtectionRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE276BE92A77F823009167B6 /* NetworkProtectionRootViewController.swift */; };
EE3766DE2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */; };
EE3B226B29DE0F110082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; };
Expand Down Expand Up @@ -2393,6 +2394,7 @@
EE0153EE2A70021E002A8B26 /* NetworkProtectionInviteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteView.swift; sourceTree = "<group>"; };
EE01EB3F2AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNSettingsViewModel.swift; sourceTree = "<group>"; };
EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNLocationView.swift; sourceTree = "<group>"; };
EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = "<group>"; };
EE276BE92A77F823009167B6 /* NetworkProtectionRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRootViewController.swift; sourceTree = "<group>"; };
EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationPresenter.swift; sourceTree = "<group>"; };
EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockInternalUserStoring.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4496,6 +4498,7 @@
children = (
EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */,
EEC02C132B0519DE0045CE11 /* NetworkProtectionVPNLocationViewModel.swift */,
EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */,
);
name = PreferredLocation;
sourceTree = "<group>";
Expand Down Expand Up @@ -6357,6 +6360,7 @@
1EE7C299294227EC0026C8CB /* AutoconsentSettingsViewController.swift in Sources */,
4BCD14632B05AF2B000B1E4C /* NetworkProtectionAccessController.swift in Sources */,
1E8AD1D527C2E22900ABA377 /* DownloadsListSectionViewModel.swift in Sources */,
EE0798C52B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */,
4BC6DD1C2A60E6AD001EC129 /* ReportBrokenSiteView.swift in Sources */,
31584616281AFB46004ADB8B /* AutofillLoginDetailsViewController.swift in Sources */,
C1F341C72A6924100032057B /* EmailAddressPromptViewModel.swift in Sources */,
Expand Down
42 changes: 42 additions & 0 deletions DuckDuckGo/NetworkProtectionVPNCountryLabelsModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// NetworkProtectionVPNCountryLabelsModel.swift
// DuckDuckGo
//
// Copyright © 2023 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 NetworkProtection

struct NetworkProtectionVPNCountryLabelsModel {
let emoji: String
let title: String

init(country: String) {
self.title = Locale.current.localizedString(forRegionCode: country) ?? country.capitalized
self.emoji = Self.flag(country: country)
}

private static func flag(country: String) -> String {
let flagBase = UnicodeScalar("🇦").value - UnicodeScalar("A").value

let flag = country
.uppercased()
.unicodeScalars
.compactMap({ UnicodeScalar(flagBase + $0.value)?.description })
.joined()
return flag
}
}
24 changes: 9 additions & 15 deletions DuckDuckGo/NetworkProtectionVPNLocationViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,35 +113,29 @@ private typealias CountryItem = NetworkProtectionVPNCountryItemModel
private typealias CityItem = NetworkProtectionVPNCityItemModel

struct NetworkProtectionVPNCountryItemModel: Identifiable {
private let labelsModel: NetworkProtectionVPNCountryLabelsModel

var emoji: String {
labelsModel.emoji
}
var title: String {
labelsModel.title
}
let isSelected: Bool
var id: String
let emoji: String
let title: String
let subtitle: String?
let cityPickerItems: [NetworkProtectionVPNCityItemModel]
let shouldShowPicker: Bool

fileprivate init(netPLocation: NetworkProtectionLocation, isSelected: Bool, cityPickerItems: [NetworkProtectionVPNCityItemModel]) {
self.labelsModel = .init(country: netPLocation.country)
self.isSelected = isSelected
self.id = netPLocation.country
self.title = Locale.current.localizedString(forRegionCode: id) ?? id
let hasMultipleCities = netPLocation.cities.count > 1
self.subtitle = hasMultipleCities ? UserText.netPVPNLocationCountryItemFormattedCitiesCount(netPLocation.cities.count) : nil
self.cityPickerItems = cityPickerItems
self.emoji = Self.flag(country: netPLocation.country)
self.shouldShowPicker = hasMultipleCities
}

static func flag(country: String) -> String {
let flagBase = UnicodeScalar("🇦").value - UnicodeScalar("A").value

let flag = country
.uppercased()
.unicodeScalars
.compactMap({ UnicodeScalar(flagBase + $0.value)?.description })
.joined()
return flag
}
}

struct NetworkProtectionVPNCityItemModel: Identifiable {
Expand Down
19 changes: 15 additions & 4 deletions DuckDuckGo/NetworkProtectionVPNSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,21 @@ struct NetworkProtectionVPNSettingsView: View {
List {
Section {
NavigationLink(destination: NetworkProtectionVPNLocationView()) {
HStack {
Text(UserText.netPPreferredLocationSettingTitle).daxBodyRegular().foregroundColor(.textPrimary)
Spacer()
Text(viewModel.preferredLocation).daxBodyRegular().foregroundColor(.textSecondary)
HStack(spacing: 16) {
switch viewModel.preferredLocation.icon {
case .defaultIcon:
Image("Location-Solid-24")
case .emoji(let string):
Text(string)
}
VStack(alignment: .leading) {
Text(UserText.netPPreferredLocationSettingTitle)
.daxBodyRegular()
.foregroundColor(.textPrimary)
Text(viewModel.preferredLocation.title)
.daxFootnoteRegular()
.foregroundColor(.textSecondary)
}
}
}
}
Expand Down
44 changes: 34 additions & 10 deletions DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,20 @@ final class NetworkProtectionVPNSettingsViewModel: ObservableObject {
private let settings: VPNSettings
private var cancellables: Set<AnyCancellable> = []

@Published public var preferredLocation: String = UserText.netPPreferredLocationNearest
@Published public var preferredLocation: NetworkProtectionLocationSettingsItemModel
@Published public var excludeLocalNetworks: Bool = true

init(settings: VPNSettings) {
self.settings = settings
self.preferredLocation = NetworkProtectionLocationSettingsItemModel(selectedLocation: settings.selectedLocation)
settings.selectedLocationPublisher
.map { selectedLocation in
guard let selectedLocation = selectedLocation.location else {
return UserText.netPPreferredLocationNearest
}
guard let city = selectedLocation.city else {
return Self.localizedString(forRegionCode: selectedLocation.country)
}
return "\(city), \(Self.localizedString(forRegionCode: selectedLocation.country))"
}
.receive(on: DispatchQueue.main)
.map(NetworkProtectionLocationSettingsItemModel.init(selectedLocation:))
.assign(to: \.preferredLocation, onWeaklyHeld: self)
.store(in: &cancellables)

settings.excludeLocalNetworksPublisher
.receive(on: DispatchQueue.main)
.assign(to: \.excludeLocalNetworks, onWeaklyHeld: self)
.store(in: &cancellables)
}
Expand All @@ -59,4 +54,33 @@ final class NetworkProtectionVPNSettingsViewModel: ObservableObject {
}
}

struct NetworkProtectionLocationSettingsItemModel {
enum LocationIcon {
case defaultIcon
case emoji(String)
}

let title: String
let icon: LocationIcon

init(selectedLocation: VPNSettings.SelectedLocation) {
switch selectedLocation {
case .nearest:
title = UserText.netPPreferredLocationNearest
icon = .defaultIcon
case .location(let location):
let countryLabelsModel = NetworkProtectionVPNCountryLabelsModel(country: location.country)
if let city = location.city {
title = UserText.netPVPNSettingsLocationSubtitleFormattedCityAndCountry(
city: city,
country: countryLabelsModel.title
)
} else {
title = countryLabelsModel.title
}
icon = .emoji(countryLabelsModel.emoji)
}
}
}

#endif
8 changes: 6 additions & 2 deletions DuckDuckGo/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,10 @@ In addition to the details entered into this form, your app issue report will co
static let netPStatusViewConnectionDetails = NSLocalizedString("network.protection.status.view.connection.details", value: "Connection Details", comment: "Connection details label shown in NetworkProtection's status view.")
static let netPStatusViewSettingsSectionTitle = NSLocalizedString("network.protection.status.view.settings.section.title", value: "Manage", comment: "Label shown on the title of the settings section in NetworkProtection's status view.")
static let netPVPNSettingsTitle = NSLocalizedString("network.protection.vpn.settings.title", value: "VPN Settings", comment: "Title for the VPN Settings screen.")
static func netPVPNSettingsLocationSubtitleFormattedCityAndCountry(city: String, country: String) -> String {
let localized = NSLocalizedString("network.protection.vpn.location.subtitle.formatted.city.and.country", value: "%@, %@", comment: "Subtitle for the preferred location item that formats a city and country. E.g Chicago, United States")
return localized.format(arguments: city, country)
}
static let netPVPNNotificationsTitle = NSLocalizedString("network.protection.vpn.notifications.title", value: "VPN Notifications", comment: "Title for the VPN Notifications management screen.")
static let netPStatusViewShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share Feedback", comment: "The status view 'Share Feedback' button which is shown inline on the status view after the \(netPInviteOnlyMessage) text")
static let netPStatusViewErrorConnectionFailedTitle = NSLocalizedString("network.protection.status.view.error.connection.failed.title", value: "Failed to Connect.", comment: "Generic connection failed error title shown in NetworkProtection's status view.")
Expand All @@ -653,7 +657,7 @@ In addition to the details entered into this form, your app issue report will co
static let netPPreferredLocationNearest = NSLocalizedString("network.protection.vpn.preferred.location.nearest", value: "Nearest Available", comment: "Label for the Preferred Location VPN Settings item when the nearest available location is selected.")
static let netPVPNLocationTitle = NSLocalizedString("network.protection.vpn.location.title", value: "VPN Location", comment: "Title for the VPN Location screen.")
static let netPVPNLocationRecommendedSectionTitle = NSLocalizedString("network.protection.vpn.location.recommended.section.title", value: "Recommended", comment: "Title for the VPN Location screen's Recommended section.")
static let netPVPNLocationRecommendedSectionFooter = NSLocalizedString("network.protection.vpn.location.recommended.section.footer", value: "Automatically connect to the nearest server we can find", comment: "Footer describing the VPN Location screen's Recommended section which just has Nearest Available.")
static let netPVPNLocationRecommendedSectionFooter = NSLocalizedString("network.protection.vpn.location.recommended.section.footer", value: "Automatically connect to the nearest server we can find.", comment: "Footer describing the VPN Location screen's Recommended section which just has Nearest Available.")
static let netPVPNLocationAllCountriesSectionTitle = NSLocalizedString("network.protection.vpn.location.all.countries.section.title", value: "All Countries", comment: "Title for the VPN Location screen's All Countries section.")
static let netPVPNLocationNearestAvailableItemTitle = NSLocalizedString("network.protection.vpn.location.nearest.available.item.title", value: "Nearest Available", comment: "Title for the VPN Location screen's Nearest Available selection item.")
static func netPVPNLocationCountryItemFormattedCitiesCount(_ count: Int) -> String {
Expand All @@ -662,7 +666,7 @@ In addition to the details entered into this form, your app issue report will co
}
static let netPExcludeLocalNetworksSettingTitle = NSLocalizedString("network.protection.vpn.exclude.local.networks.setting.title", value: "Exclude Local Networks", comment: "Title for the Exclude Local Networks setting item.")
static let netPExcludeLocalNetworksSettingFooter = NSLocalizedString("network.protection.vpn.exclude.local.networks.setting.footer", value: "Bypass the VPN for local network connections, like to a printer.", comment: "Footer text for the Exclude Local Networks setting item.")
static let netPSecureDNSSettingFooter = NSLocalizedString("network.protection.vpn.secure.dns.setting.footer", value: "Network Protection prevents DNS leaks to your Internet Service Provider by routing DNS queries though the VPN tunnel to our own resolver.", comment: "Footer text for the Always on VPN setting item.")
static let netPSecureDNSSettingFooter = NSLocalizedString("network.protection.vpn.secure.dns.setting.footer", value: "Our VPN uses Secure DNS to keep your online activity private, so that your Internet provider can't see what websites you visit.", comment: "Footer text for the Always on VPN setting item.")
static let netPTurnOnNotificationsButtonTitle = NSLocalizedString("network.protection.turn.on.notifications.button.title", value: "Turn on Notifications", comment: "Title for the button to link to the iOS app settings and enable notifications app-wide.")
static let netPTurnOnNotificationsSectionFooter = NSLocalizedString("network.protection.turn.on.notifications.section.footer", value: "Allow DuckDuckGo to notify you if your connection drops or VPN status changes.", comment: "Footer text under the button to link to the iOS app settings and enable notifications app-wide.")
static let netPVPNAlertsToggleTitle = NSLocalizedString("network.protection.vpn.alerts.toggle.title", value: "VPN Alerts", comment: "Title for the toggle for VPN alerts.")
Expand Down
7 changes: 5 additions & 2 deletions DuckDuckGo/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1625,11 +1625,14 @@ https://duckduckgo.com/mac";
"network.protection.vpn.location.nearest.available.item.title" = "Nearest Available";

/* Footer describing the VPN Location screen's Recommended section which just has Nearest Available. */
"network.protection.vpn.location.recommended.section.footer" = "Automatically connect to the nearest server we can find";
"network.protection.vpn.location.recommended.section.footer" = "Automatically connect to the nearest server we can find.";

/* Title for the VPN Location screen's Recommended section. */
"network.protection.vpn.location.recommended.section.title" = "Recommended";

/* Subtitle for the preferred location item that formats a city and country. E.g Chicago, United States */
"network.protection.vpn.location.subtitle.formatted.city.and.country" = "%1$@, %2$@";

/* Title for the VPN Location screen. */
"network.protection.vpn.location.title" = "VPN Location";

Expand All @@ -1643,7 +1646,7 @@ https://duckduckgo.com/mac";
"network.protection.vpn.preferred.location.title" = "Preferred Location";

/* Footer text for the Always on VPN setting item. */
"network.protection.vpn.secure.dns.setting.footer" = "Network Protection prevents DNS leaks to your Internet Service Provider by routing DNS queries though the VPN tunnel to our own resolver.";
"network.protection.vpn.secure.dns.setting.footer" = "Our VPN uses Secure DNS to keep your online activity private, so that your Internet provider can't see what websites you visit.";

/* Title for the VPN Settings screen. */
"network.protection.vpn.settings.title" = "VPN Settings";
Expand Down

0 comments on commit 82c8517

Please sign in to comment.