diff --git a/.github/workflows/sync-end-to-end.yml b/.github/workflows/sync-end-to-end.yml index df2a04df7c..3dc49732fa 100644 --- a/.github/workflows/sync-end-to-end.yml +++ b/.github/workflows/sync-end-to-end.yml @@ -60,4 +60,23 @@ jobs: env: | CODE=${{ steps.sync-recovery-code.outputs.recovery-code }} + - name: Create Asana task when workflow failed + if: ${{ failure() }} + run: | + curl -s "https://app.asana.com/api/1.0/tasks" \ + --header "Accept: application/json" \ + --header "Authorization: Bearer ${{ secrets.ASANA_ACCESS_TOKEN }}" \ + --header "Content-Type: application/json" \ + --data ' { "data": { "name": "GH Workflow Failure - Sync End to end tests", "workspace": "${{ vars.GH_ASANA_WORKSPACE_ID }}", "projects": [ "${{ vars.GH_ASANA_IOS_APP_PROJECT_ID }}" ], "notes" : "The end to end workflow has failed. See https://github.com/duckduckgo/iOS/actions/runs/${{ github.run_id }}" } }' + + - name: Upload logs when workflow failed + uses: actions/upload-artifact@v3 + if: failure() + with: + name: BuildLogs + path: | + xcodebuild.log + DerivedData/Logs/Test/*.xcresult + retention-days: 7 + diff --git a/.maestro/shared/sync_create.yaml b/.maestro/shared/sync_create.yaml index 8164466ac3..bc4bb338d0 100644 --- a/.maestro/shared/sync_create.yaml +++ b/.maestro/shared/sync_create.yaml @@ -1,12 +1,10 @@ appId: com.duckduckgo.mobile.ios --- -- assertVisible: Sync -- tapOn: Sync -- assertVisible: Sync -- tapOn: "0" -- assertVisible: Turn on Sync? -- tapOn: Turn on Sync -- tapOn: Sync Another Device -- tapOn: Show QR Code -- assertVisible: "Go to Settings > Sync in the DuckDuckGo App on a different device and scan this QR code to sync." \ No newline at end of file +- assertVisible: Sync & Back Up +- tapOn: Sync & Back Up +- assertVisible: Sync & Back Up +- tapOn: Start Sync & Back Up +- assertVisible: All Set! +- tapOn: Next +- assertVisible: Save Recovery Code? diff --git a/.maestro/shared/sync_delete.yaml b/.maestro/shared/sync_delete.yaml index a82919c953..54a78f3c15 100644 --- a/.maestro/shared/sync_delete.yaml +++ b/.maestro/shared/sync_delete.yaml @@ -1,7 +1,7 @@ appId: com.duckduckgo.mobile.ios --- -- assertVisible: Sync +- assertVisible: Sync & Back Up - scroll - tapOn: point: 50%,91% # TODO: Revisit after new setup flow has been implemented. diff --git a/.maestro/sync_tests/01_create_account.yaml b/.maestro/sync_tests/01_create_account.yaml index 6e39ee2fd4..5b48e6c829 100644 --- a/.maestro/sync_tests/01_create_account.yaml +++ b/.maestro/sync_tests/01_create_account.yaml @@ -21,9 +21,7 @@ tags: # Clean up -- tapOn: Back -- tapOn: Cancel - tapOn: Not Now -- assertVisible: Sync +- assertVisible: Sync & Back Up - runFlow: - file: ../shared/sync_delete.yaml \ No newline at end of file + file: ../shared/sync_delete.yaml diff --git a/.maestro/sync_tests/02_login_account.yaml b/.maestro/sync_tests/02_login_account.yaml index aef46f2912..99a69684fa 100644 --- a/.maestro/sync_tests/02_login_account.yaml +++ b/.maestro/sync_tests/02_login_account.yaml @@ -21,25 +21,22 @@ tags: file: ../shared/sync_create.yaml # Copy Sync Code and Log Out -- tapOn: Back -- tapOn: Cancel -- assertVisible: Save Recovery Key -- tapOn: Copy Key +- tapOn: Copy Code - tapOn: Not Now -- tapOn: "1" +- assertVisible: Sync & Back Up +- tapOn: Turn Off Sync & Back Up - assertVisible: Turn Off Sync? - tapOn: Remove # Login -- tapOn: "0" -- tapOn: Recover Your Synced Data -- tapOn: Manually Enter Code +- assertVisible: Sync & Back Up +- tapOn: Enter Text Code - tapOn: Paste - assertVisible: Device Synced! - tapOn: Next - tapOn: Not Now # Clean up -- assertVisible: Sync +- assertVisible: Sync & Back Up - runFlow: - file: ../shared/sync_delete.yaml \ No newline at end of file + file: ../shared/sync_delete.yaml diff --git a/.maestro/sync_tests/03_recover_account.yaml b/.maestro/sync_tests/03_recover_account.yaml index 265684b884..d20cbf3b3b 100644 --- a/.maestro/sync_tests/03_recover_account.yaml +++ b/.maestro/sync_tests/03_recover_account.yaml @@ -19,8 +19,12 @@ tags: - tapOn: id: searchEntry - inputText: ${CODE} -- longPressOn: - id: searchEntry +- repeat: + while: + notVisible: "Select All" + commands: + - tapOn: + id: searchEntry - tapOn: Select All - tapOn: Cut - tapOn: @@ -35,14 +39,11 @@ tags: - tapOn: Settings - runFlow: file: ../shared/set_internal_user.yaml -- assertVisible: Sync -- tapOn: Sync -- assertVisible: Sync -- tapOn: "0" -- assertVisible: Turn on Sync? -- tapOn: Recover Your Synced Data -- assertVisible: Scan QR Code -- tapOn: Manually Enter Code +- assertVisible: Sync & Back Up +- tapOn: Sync & Back Up +- assertVisible: Sync & Back up +- tapOn: Recover Your Data +- tapOn: Enter Text Code - tapOn: Paste - assertVisible: Device Synced! - tapOn: Next diff --git a/.maestro/sync_tests/04_sync_data.yaml b/.maestro/sync_tests/04_sync_data.yaml index 643cd1a431..329b22bb80 100644 --- a/.maestro/sync_tests/04_sync_data.yaml +++ b/.maestro/sync_tests/04_sync_data.yaml @@ -56,14 +56,12 @@ tags: - tapOn: id: searchEntry - inputText: ${CODE} -- longPressOn: - id: searchEntry -- runFlow: - when: - visible: - text: searchEntry +- repeat: + while: + notVisible: "Select All" commands: - - tapOn: searchEntry + - tapOn: + id: searchEntry - tapOn: Select All - tapOn: Cut - tapOn: @@ -86,14 +84,11 @@ tags: - tapOn: Settings - runFlow: file: ../shared/set_internal_user.yaml -- assertVisible: Sync -- tapOn: Sync -- assertVisible: Sync -- tapOn: "0" -- assertVisible: Turn on Sync? -- tapOn: Recover Your Synced Data -- assertVisible: Scan QR Code -- tapOn: Manually Enter Code +- assertVisible: Sync & Back Up +- tapOn: Sync & Back Up +- assertVisible: Sync & Back up +- tapOn: Recover Your Data +- tapOn: Enter Text Code - tapOn: Paste - assertVisible: Device Synced! - tapOn: Next diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index a2005a22f2..057446861c 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -312,6 +312,8 @@ extension Pixel { case networkProtectionClientFailedToEncodeRegisterKeyRequest case networkProtectionClientFailedToFetchRegisteredServers case networkProtectionClientFailedToParseRegisteredServersResponse + case networkProtectionClientFailedToFetchLocations + case networkProtectionClientFailedToParseLocationsResponse case networkProtectionClientFailedToEncodeRedeemRequest case networkProtectionClientInvalidInviteCode case networkProtectionClientFailedToRedeemInviteCode @@ -810,6 +812,9 @@ extension Pixel.Event { case .networkProtectionClientFailedToFetchRegisteredServers: return "m_netp_backend_api_error_failed_to_fetch_registered_servers" case .networkProtectionClientFailedToParseRegisteredServersResponse: return "m_netp_backend_api_error_parsing_device_registration_response_failed" + case .networkProtectionClientFailedToFetchLocations: return "m_netp_backend_api_error_failed_to_fetch_locations" + case .networkProtectionClientFailedToParseLocationsResponse: + return "m_netp_backend_api_error_parsing_locations_response_failed" case .networkProtectionClientFailedToEncodeRedeemRequest: return "m_netp_backend_api_error_encoding_redeem_request_body_failed" case .networkProtectionClientInvalidInviteCode: return "m_netp_backend_api_error_invalid_invite_code" case .networkProtectionClientFailedToRedeemInviteCode: return "m_netp_backend_api_error_failed_to_redeem_invite_code" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 4f65ad8b0a..034d39e66e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -758,6 +758,8 @@ EE0153EB2A6FF970002A8B26 /* NetworkProtectionRootViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153EA2A6FF970002A8B26 /* NetworkProtectionRootViewModelTests.swift */; }; EE0153ED2A6FF9E6002A8B26 /* NetworkProtectionRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0153EC2A6FF9E6002A8B26 /* NetworkProtectionRootView.swift */; }; 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 */; }; 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 */; }; @@ -2361,6 +2363,8 @@ EE0153EA2A6FF970002A8B26 /* NetworkProtectionRootViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRootViewModelTests.swift; sourceTree = ""; }; EE0153EC2A6FF9E6002A8B26 /* NetworkProtectionRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRootView.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -4441,6 +4445,14 @@ name = Root; sourceTree = ""; }; + EE01EB412AFC1DE10096AAC9 /* PreferredLocation */ = { + isa = PBXGroup; + children = ( + EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */, + ); + name = PreferredLocation; + sourceTree = ""; + }; EE3766DC2AC5940A00AAB575 /* NetworkProtection */ = { isa = PBXGroup; children = ( @@ -4499,6 +4511,7 @@ isa = PBXGroup; children = ( EE9D68D02AE00CF300B55EF4 /* NetworkProtectionVPNSettingsView.swift */, + EE01EB3F2AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift */, ); name = VPNSettings; sourceTree = ""; @@ -4515,6 +4528,7 @@ EECD94B22A28B8580085C66E /* NetworkProtection */ = { isa = PBXGroup; children = ( + EE01EB412AFC1DE10096AAC9 /* PreferredLocation */, EE9D68D62AE1527F00B55EF4 /* VPNNotifications */, EE9D68CF2AE00CE000B55EF4 /* VPNSettings */, EE458D122ABB651500FC651A /* Debug */, @@ -6444,6 +6458,7 @@ F1D796F01E7B07610019D451 /* BookmarksViewControllerCells.swift in Sources */, 85058369219F424500ED4EDB /* UIColorExtension.swift in Sources */, 85058368219C49E000ED4EDB /* HomeViewSectionRenderers.swift in Sources */, + EE01EB432AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift in Sources */, F456B3B525810BB900B79B90 /* FireButtonAnimationSettingsViewController.swift in Sources */, 9820EAF522613CD30089094D /* WebProgressWorker.swift in Sources */, B6CB93E5286445AB0090FEB4 /* Base64DownloadSession.swift in Sources */, @@ -6514,6 +6529,7 @@ 31EF52E1281B3BDC0034796E /* AutofillLoginListItemViewModel.swift in Sources */, 1E4FAA6627D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift in Sources */, 83004E862193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift in Sources */, + EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */, EE72CA852A862D000043B5B3 /* NetworkProtectionDebugViewController.swift in Sources */, C18ED43A2AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift in Sources */, CB84C7BD29A3EF530088A5B8 /* AppConfigurationURLProvider.swift in Sources */, @@ -9115,7 +9131,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 83.0.0; + version = 84.0.0; }; }; C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d9cc92bff7..6c8f3f340a 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/DuckDuckGo/BrowserServicesKit", "state": { "branch": null, - "revision": "f7e20cd37bbc0d25ae3c3f25ef52d319366613e7", - "version": "83.0.0" + "revision": "9c2c7f39679a1f4441fec95fda86f4c089724e2e", + "version": "84.0.0" } }, { diff --git a/DuckDuckGo/BrokenSiteInfo.swift b/DuckDuckGo/BrokenSiteInfo.swift index fc516b48ef..2f310936e3 100644 --- a/DuckDuckGo/BrokenSiteInfo.swift +++ b/DuckDuckGo/BrokenSiteInfo.swift @@ -99,7 +99,6 @@ public struct BrokenSiteInfo { Keys.tds: tdsETag?.trimmingCharacters(in: CharacterSet(charactersIn: "\"")) ?? "", Keys.blockedTrackers: blockedTrackerDomains.joined(separator: ","), Keys.surrogates: installedSurrogates.joined(separator: ","), - Keys.atb: StatisticsUserDefaults().atb ?? "", Keys.os: systemVersion, Keys.manufacturer: manufacturer, Keys.model: model, diff --git a/DuckDuckGo/EventMapping+NetworkProtectionError.swift b/DuckDuckGo/EventMapping+NetworkProtectionError.swift index a131d64443..cefbd105ec 100644 --- a/DuckDuckGo/EventMapping+NetworkProtectionError.swift +++ b/DuckDuckGo/EventMapping+NetworkProtectionError.swift @@ -32,6 +32,12 @@ extension EventMapping where Event == NetworkProtectionError { var params: [String: String] = [:] switch event { + case .failedToFetchLocationList(let error): + pixelEvent = .networkProtectionClientFailedToFetchLocations + pixelError = error + case .failedToParseLocationListResponse(let error): + pixelEvent = .networkProtectionClientFailedToParseLocationsResponse + pixelError = error case .failedToEncodeRedeemRequest: pixelEvent = .networkProtectionClientFailedToEncodeRedeemRequest case .invalidInviteCode: @@ -89,7 +95,6 @@ extension EventMapping where Event == NetworkProtectionError { // Should never be sent from from the app case .unhandledError(function: let function, line: let line, error: let error): pixelEvent = .networkProtectionUnhandledError - } DailyPixel.fireDailyAndCount(pixel: pixelEvent, error: pixelError, withAdditionalParameters: params) diff --git a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift index 0587a405b8..8cddce00d9 100644 --- a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift +++ b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift @@ -54,7 +54,12 @@ extension NetworkProtectionKeychainTokenStore { extension NetworkProtectionCodeRedemptionCoordinator { convenience init() { - self.init(tokenStore: NetworkProtectionKeychainTokenStore(), errorEvents: .networkProtectionAppDebugEvents) + let tunnelSettings = TunnelSettings(defaults: .networkProtectionGroupDefaults) + self.init( + environment: tunnelSettings.selectedEnvironment, + tokenStore: NetworkProtectionKeychainTokenStore(), + errorEvents: .networkProtectionAppDebugEvents + ) } } @@ -68,4 +73,26 @@ extension NetworkProtectionVPNNotificationsViewModel { } } +extension NetworkProtectionVPNSettingsViewModel { + convenience init() { + self.init( + tunnelSettings: TunnelSettings(defaults: .networkProtectionGroupDefaults) + ) + } +} + +extension NetworkProtectionVPNLocationViewModel { + convenience init() { + let tunnelSettings = TunnelSettings(defaults: .networkProtectionGroupDefaults) + let locationListRepository = NetworkProtectionLocationListCompositeRepository( + environment: tunnelSettings.selectedEnvironment, + tokenStore: NetworkProtectionKeychainTokenStore() + ) + self.init( + locationListRepository: locationListRepository, + tunnelSettings: TunnelSettings(defaults: .networkProtectionGroupDefaults) + ) + } +} + #endif diff --git a/DuckDuckGo/NetworkProtectionVPNLocationView.swift b/DuckDuckGo/NetworkProtectionVPNLocationView.swift new file mode 100644 index 0000000000..b9d2a337fd --- /dev/null +++ b/DuckDuckGo/NetworkProtectionVPNLocationView.swift @@ -0,0 +1,100 @@ +// +// NetworkProtectionVPNLocationView.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. +// + +#if NETWORK_PROTECTION + +import Foundation +import SwiftUI + +@available(iOS 15, *) +struct NetworkProtectionVPNLocationView: View { + @StateObject var model = NetworkProtectionVPNLocationViewModel() + + var body: some View { + List { + Text("⚠️ FEATURE IS WORK IN PROGRESS ⚠️") + Section { + Button(action: model.onNearestItemSelection) { + Text(UserText.netPPreferredLocationNearest) + } + } + Section { + ForEach(model.countryItems) { item in + Button(action: { + model.onCountryItemSelection(countryID: item.countryID) + }, label: { + Text(item.localizedName) + }) + } + } + } + .animation(.default, value: model.countryItems.isEmpty) + .applyInsetGroupedListStyle() + .navigationTitle("VPN Location").onAppear { + Task { + await model.onViewAppeared() + } + } + } +} + +import NetworkProtection + +final class NetworkProtectionVPNLocationViewModel: ObservableObject { + private let locationListRepository: NetworkProtectionLocationListRepository + private let tunnelSettings: TunnelSettings + @Published public var countryItems: [NetworkProtectionVPNCountryItemModel] = [] + + init(locationListRepository: NetworkProtectionLocationListRepository, tunnelSettings: TunnelSettings) { + self.locationListRepository = locationListRepository + self.tunnelSettings = tunnelSettings + } + + @MainActor + func onViewAppeared() async { + guard let list = try? await locationListRepository.fetchLocationList() else { return } + self.countryItems = list.map(NetworkProtectionVPNCountryItemModel.init(netPLocation:)) + } + + func onNearestItemSelection() { + tunnelSettings.selectedLocation = .nearest + } + + func onCountryItemSelection(countryID: String) { + let location = NetworkProtectionSelectedLocation(country: countryID) + tunnelSettings.selectedLocation = .location(location) + } +} + +struct NetworkProtectionVPNCountryItemModel: Identifiable { + let countryID: String + let localizedName: String + let cities: [String] + var id: String { + "\(countryID) - \(cities.count) cities" + } + + init(netPLocation: NetworkProtectionLocation) { + self.countryID = netPLocation.country + self.localizedName = Locale.current.localizedString(forRegionCode: countryID) ?? countryID + self.cities = netPLocation.cities.map(\.name) + } +} + +#endif diff --git a/DuckDuckGo/NetworkProtectionVPNSettingsView.swift b/DuckDuckGo/NetworkProtectionVPNSettingsView.swift index b2dc110fda..f22075f044 100644 --- a/DuckDuckGo/NetworkProtectionVPNSettingsView.swift +++ b/DuckDuckGo/NetworkProtectionVPNSettingsView.swift @@ -20,12 +20,21 @@ #if NETWORK_PROTECTION import SwiftUI +import DesignResourcesKit @available(iOS 15, *) struct NetworkProtectionVPNSettingsView: View { + @StateObject var viewModel = NetworkProtectionVPNSettingsViewModel() var body: some View { List { + NavigationLink(destination: NetworkProtectionVPNLocationView()) { + HStack { + Text(UserText.netPPreferredLocationSettingTitle).daxBodyRegular().foregroundColor(.textPrimary) + Spacer() + Text(viewModel.preferredLocation).daxBodyRegular().foregroundColor(.textSecondary) + } + } toggleSection( text: UserText.netPAlwaysOnSettingTitle, footerText: UserText.netPAlwaysOnSettingFooter diff --git a/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift b/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift new file mode 100644 index 0000000000..19664143e8 --- /dev/null +++ b/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift @@ -0,0 +1,50 @@ +// +// NetworkProtectionVPNSettingsViewModel.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. +// + +#if NETWORK_PROTECTION + +import Foundation +import NetworkProtection +import Combine + +final class NetworkProtectionVPNSettingsViewModel: ObservableObject { + private let tunnelSettings: TunnelSettings + private var cancellable: AnyCancellable? + + @Published public var preferredLocation: String = UserText.netPPreferredLocationNearest + + init(tunnelSettings: TunnelSettings) { + self.tunnelSettings = tunnelSettings + cancellable = tunnelSettings.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))" + }.assign(to: \.preferredLocation, onWeaklyHeld: self) + } + + private static func localizedString(forRegionCode: String) -> String { + Locale.current.localizedString(forRegionCode: forRegionCode) ?? forRegionCode.capitalized + } +} + +#endif diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index afb25e0e08..c333c8f991 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -652,6 +652,12 @@ In addition to the details entered into this form, your app issue report will co 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.") static let netPStatusViewErrorConnectionFailedMessage = NSLocalizedString("network.protection.status.view.error.connection.failed.message", value: "Please try again later.", comment: "Generic connection failed error message shown in NetworkProtection's status view.") + static let netPPreferredLocationSettingTitle = NSLocalizedString("network.protection.vpn.preferred.location.title", value: "Preferred Location", comment: "Title for the Preferred Location VPN Settings item.") + 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 netPVPNLocationRecommendedSectionTitle = NSLocalizedString("network.protection.vpn.location.recommended.section.title", value: "Recommended", comment: "Title for the VPN Location screen's Recommended section.") + 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 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 netPAlwaysOnSettingTitle = NSLocalizedString("network.protection.vpn.always.on.setting.title", value: "Always On", comment: "Title for the Always on VPN setting item.") static let netPAlwaysOnSettingFooter = NSLocalizedString("network.protection.vpn.always.on.setting.footer", value: "Automatically restore a VPN connection after interruption.", comment: "Footer text for the Always on VPN setting item.") static let netPSecureDNSSettingTitle = NSLocalizedString("network.protection.vpn.secure.dns.setting.title", value: "Secure DNS", comment: "Title for the Always on VPN setting item.") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index aeac08a7bc..bb9001ad12 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -1516,9 +1516,27 @@ https://duckduckgo.com/mac"; /* Title for the Always on VPN setting item. */ "network.protection.vpn.always.on.setting.title" = "Always On"; +/* Title for the VPN Location screen's All Countries section. */ +"network.protection.vpn.location.all.countries.section.title" = "All Countries"; + +/* Title for the VPN Location screen's Nearest Available selection item. */ +"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"; + +/* Title for the VPN Location screen's Recommended section. */ +"network.protection.vpn.location.recommended.section.title" = "Recommended"; + /* Title for the VPN Notifications management screen. */ "network.protection.vpn.notifications.title" = "VPN Notifications"; +/* Label for the Preferred Location VPN Settings item when the nearest available location is selected. */ +"network.protection.vpn.preferred.location.nearest" = "Nearest Available"; + +/* Title for the Preferred Location VPN Settings item. */ +"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."; diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index d3ca118411..4943d79eb3 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -140,6 +140,10 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { params[PixelParameters.function] = function params[PixelParameters.line] = String(line) pixelError = error + case .failedToFetchLocationList: + return + case .failedToParseLocationListResponse: + return } DailyPixel.fireDailyAndCount(pixel: pixelEvent, error: pixelError, withAdditionalParameters: params) } @@ -184,7 +188,8 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { keychainType: .dataProtection(.unspecified), tokenStore: tokenStore, debugEvents: Self.networkProtectionDebugEvents(controllerErrorStore: errorStore), - providerEvents: Self.packetTunnelProviderEvents) + providerEvents: Self.packetTunnelProviderEvents, + tunnelSettings: TunnelSettings(defaults: .networkProtectionGroupDefaults)) startMonitoringMemoryPressureEvents() observeServerChanges() APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent)