Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit 2982a23

Browse files
authored
VPN Geoswitching - initial draft (#1978)
Task/Issue URL: https://app.asana.com/0/0/1206183910299715/f Description: This adds an initial implementation of location switching for the VPN. Known issues: Some of the layout / spacing is not quite right The tap area of the items on the VPN Location selection screen is too small The styling of the list sections on the VPN Location screen is not finished. There seems to be a bug where the initial selection is not respected when connecting after selecting There is a momentary delay before the list items load. Needs unit tests (I promise I will add them, they are very similar to the iOS ones so it won’t take long. I just ran out of time before the holidays).
1 parent 91324f4 commit 2982a23

File tree

10 files changed

+690
-1
lines changed

10 files changed

+690
-1
lines changed

DuckDuckGo.xcodeproj/project.pbxproj

+48
Large diffs are not rendered by default.

DuckDuckGo/Common/Localizables/UserText.swift

+24
Original file line numberDiff line numberDiff line change
@@ -302,10 +302,34 @@ struct UserText {
302302

303303
// VPN Setting Titles
304304

305+
static let vpnLocationTitle = NSLocalizedString("vpn.location.title", value: "Location", comment: "Location section title in VPN settings")
305306
static let vpnGeneralTitle = NSLocalizedString("vpn.general.title", value: "General", comment: "General section title in VPN settings")
306307
static let vpnNotificationsSettingsTitle = NSLocalizedString("vpn.notifications.settings.title", value: "Notifications", comment: "Notifications section title in VPN settings")
307308
static let vpnAdvancedSettingsTitle = NSLocalizedString("vpn.advanced.settings.title", value: "Advanced", comment: "VPN Advanced section title in VPN settings")
308309

310+
// VPN Location
311+
312+
static let vpnLocationChangeButtonTitle = NSLocalizedString("vpn.location.change.button.title", value: "Change...", comment: "Title of the VPN location preference change button")
313+
static let vpnLocationListTitle = NSLocalizedString("vpn.location.list.title", value: "VPN Location", comment: "Title of the VPN location list screen")
314+
static let vpnLocationRecommendedSectionTitle = NSLocalizedString("vpn.location.recommended.section.title", value: "Recommended", comment: "Title of the VPN location list recommended section")
315+
static let vpnLocationCustomSectionTitle = NSLocalizedString("vpn.location.custom.section.title", value: "Custom", comment: "Title of the VPN location list custom section")
316+
static let vpnLocationSubmitButtonTitle = NSLocalizedString("vpn.location.submit.button.title", value: "Submit", comment: "Title of the VPN location list submit button")
317+
static let vpnLocationCancelButtonTitle = NSLocalizedString("vpn.location.custom.section.title", value: "Cancel", comment: "Title of the VPN location list cancel button")
318+
static let vpnLocationNearest = NSLocalizedString(
319+
"vpn.location.description.nearest",
320+
value: "Nearest",
321+
comment: "Nearest city setting description")
322+
static let vpnLocationNearestAvailable = NSLocalizedString(
323+
"vpn.location.description.nearest.available",
324+
value: "Nearest Available",
325+
comment: "Nearest available location setting description")
326+
static let vpnLocationNearestAvailableSubtitle = NSLocalizedString("vpn.location.nearest.available.title", value: "Automatically connect to the nearest server we can find.", comment: "Subtitle underneath the nearest available vpn location preference text.")
327+
328+
static func vpnLocationCountryItemFormattedCitiesCount(_ count: Int) -> String {
329+
let message = NSLocalizedString("network.protection.vpn.location.country.item.formatted.cities.count", value: "%d cities", comment: "Subtitle of countries item when there are multiple cities, example : ")
330+
return String(format: message, count)
331+
}
332+
309333
// VPN Settings
310334

311335
static let vpnConnectOnLoginSettingTitle = NSLocalizedString(

DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public enum FeatureFlag: String {
2525
/// Add experimental atb parameter to SERP queries for internal users to display Privacy Reminder
2626
/// https://app.asana.com/0/1199230911884351/1205979030848528/f
2727
case appendAtbToSerpQueries
28+
29+
case vpnGeoswitching
2830
}
2931

3032
extension FeatureFlag: FeatureFlagSourceProviding {
@@ -34,6 +36,8 @@ extension FeatureFlag: FeatureFlagSourceProviding {
3436
return .internalOnly
3537
case .appendAtbToSerpQueries:
3638
return .internalOnly
39+
case .vpnGeoswitching:
40+
return .internalOnly
3741
}
3842
}
3943
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// NetworkProtectionVPNCountryLabelsModel.swift
3+
//
4+
// Copyright © 2023 DuckDuckGo. All rights reserved.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
#if NETWORK_PROTECTION
20+
21+
import Foundation
22+
import NetworkProtection
23+
24+
struct NetworkProtectionVPNCountryLabelsModel {
25+
let emoji: String
26+
let title: String
27+
28+
init(country: String) {
29+
self.title = Locale.current.localizedString(forRegionCode: country) ?? country.capitalized
30+
self.emoji = Self.flag(country: country)
31+
}
32+
33+
private static func flag(country: String) -> String {
34+
let flagBase = UnicodeScalar("🇦").value - UnicodeScalar("A").value
35+
36+
let flag = country
37+
.uppercased()
38+
.unicodeScalars
39+
.compactMap({ UnicodeScalar(flagBase + $0.value)?.description })
40+
.joined()
41+
return flag
42+
}
43+
}
44+
45+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// NetworkProtectionVPNLocationPreferenceItem.swift
3+
//
4+
// Copyright © 2023 DuckDuckGo. All rights reserved.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
#if NETWORK_PROTECTION
20+
21+
import Foundation
22+
import SwiftUI
23+
24+
struct VPNLocationPreferenceItem: View {
25+
let model: VPNLocationPreferenceItemModel
26+
@State private var isShowingLocationSheet: Bool = false
27+
28+
var body: some View {
29+
VStack(alignment: .leading) {
30+
HStack(spacing: 10) {
31+
switch model.icon {
32+
case .defaultIcon:
33+
Image(systemName: "location.fill")
34+
.resizable()
35+
.frame(width: 18, height: 18)
36+
case .emoji(let string):
37+
Text(string).font(.system(size: 20))
38+
}
39+
40+
VStack(alignment: .leading) {
41+
Text(model.title)
42+
.font(.system(size: 13))
43+
.foregroundColor(.primary)
44+
if let subtitle = model.subtitle {
45+
Text(subtitle)
46+
.font(.system(size: 11))
47+
.foregroundColor(.secondary)
48+
}
49+
}
50+
Spacer()
51+
Button(UserText.vpnLocationChangeButtonTitle) {
52+
isShowingLocationSheet = true
53+
}
54+
.sheet(isPresented: $isShowingLocationSheet) {
55+
VPNLocationView(isPresented: $isShowingLocationSheet)
56+
}
57+
}
58+
}
59+
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .topLeading)
60+
.padding(10)
61+
.background(Color("BlackWhite1"))
62+
.roundedBorder()
63+
}
64+
65+
}
66+
67+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// NetworkProtectionLocationSettingsItemModel.swift
3+
//
4+
// Copyright © 2023 DuckDuckGo. All rights reserved.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
#if NETWORK_PROTECTION
20+
21+
import Foundation
22+
import NetworkProtection
23+
24+
struct VPNLocationPreferenceItemModel {
25+
enum LocationIcon {
26+
case defaultIcon
27+
case emoji(String)
28+
}
29+
30+
let title: String
31+
let subtitle: String?
32+
let icon: LocationIcon
33+
34+
init(selectedLocation: VPNSettings.SelectedLocation) {
35+
switch selectedLocation {
36+
case .nearest:
37+
title = UserText.vpnLocationNearestAvailable
38+
subtitle = UserText.vpnLocationNearestAvailableSubtitle
39+
icon = .defaultIcon
40+
case .location(let location):
41+
let countryLabelsModel = NetworkProtectionVPNCountryLabelsModel(country: location.country)
42+
title = countryLabelsModel.title
43+
subtitle = selectedLocation.location?.city
44+
icon = .emoji(countryLabelsModel.emoji)
45+
}
46+
}
47+
}
48+
49+
#endif

0 commit comments

Comments
 (0)