diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 489a3abd77..feaaa47f33 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -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 */; }; @@ -2393,6 +2394,7 @@ EE0153EE2A70021E002A8B26 /* NetworkProtectionInviteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteView.swift; sourceTree = ""; }; EE01EB3F2AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNSettingsViewModel.swift; sourceTree = ""; }; EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNLocationView.swift; sourceTree = ""; }; + EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; EE276BE92A77F823009167B6 /* NetworkProtectionRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRootViewController.swift; sourceTree = ""; }; EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationPresenter.swift; sourceTree = ""; }; EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockInternalUserStoring.swift; sourceTree = ""; }; @@ -4496,6 +4498,7 @@ children = ( EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */, EEC02C132B0519DE0045CE11 /* NetworkProtectionVPNLocationViewModel.swift */, + EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */, ); name = PreferredLocation; sourceTree = ""; @@ -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 */, diff --git a/DuckDuckGo/NetworkProtectionVPNCountryLabelsModel.swift b/DuckDuckGo/NetworkProtectionVPNCountryLabelsModel.swift new file mode 100644 index 0000000000..0d450d449c --- /dev/null +++ b/DuckDuckGo/NetworkProtectionVPNCountryLabelsModel.swift @@ -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 + } +} diff --git a/DuckDuckGo/NetworkProtectionVPNLocationViewModel.swift b/DuckDuckGo/NetworkProtectionVPNLocationViewModel.swift index 38dfdd1b66..0a13bf75cb 100644 --- a/DuckDuckGo/NetworkProtectionVPNLocationViewModel.swift +++ b/DuckDuckGo/NetworkProtectionVPNLocationViewModel.swift @@ -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 { diff --git a/DuckDuckGo/NetworkProtectionVPNSettingsView.swift b/DuckDuckGo/NetworkProtectionVPNSettingsView.swift index f7b5c33774..74a50a3c95 100644 --- a/DuckDuckGo/NetworkProtectionVPNSettingsView.swift +++ b/DuckDuckGo/NetworkProtectionVPNSettingsView.swift @@ -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) + } } } } diff --git a/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift b/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift index f7ff89c53f..b0ac27cf07 100644 --- a/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift +++ b/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift @@ -27,25 +27,20 @@ final class NetworkProtectionVPNSettingsViewModel: ObservableObject { private let settings: VPNSettings private var cancellables: Set = [] - @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) } @@ -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 diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 0a67d5dcc8..5a03382223 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -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.") @@ -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 { @@ -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.") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index aa23444dbd..b9cf565057 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -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"; @@ -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";