diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml index 7883f9200c..bd23a9604b 100644 --- a/.github/workflows/code_freeze.yml +++ b/.github/workflows/code_freeze.yml @@ -58,6 +58,12 @@ jobs: | jq -r .data.new_task.gid)" echo "asana_task_url=https://app.asana.com/0/0/${asana_task_id}/f" >> $GITHUB_OUTPUT + curl -fLSs -X POST "https://app.asana.com/api/1.0/sections/${{ vars.MACOS_APP_DEVELOPMENT_RELEASE_SECTION_ID }}/addTask" \ + -H "Authorization: Bearer ${{ env.ASANA_ACCESS_TOKEN }}" \ + -H "Content-Type: application/json" \ + --output /dev/null \ + -d "{\"data\": {\"task\": \"${asana_task_id}\"}}" + assignee_id="$(curl -fLSs https://raw.githubusercontent.com/duckduckgo/BrowserServicesKit/main/.github/actions/asana-failed-pr-checks/user_ids.json \ | jq -r .${{ github.actor }})" diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 60e87698ab..4d07d781a0 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 93 +CURRENT_PROJECT_VERSION = 95 diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index cfe2587555..a331487eef 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.68.0 +MARKETING_VERSION = 1.69.0 diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 497db1cc7a..ad370544c5 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2949,6 +2949,9 @@ B6FA893D269C423100588ECD /* PrivacyDashboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */; }; B6FA893F269C424500588ECD /* PrivacyDashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */; }; B6FA8941269C425400588ECD /* PrivacyDashboardPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */; }; + BBDFDC5A2B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; + BBDFDC5C2B2B8D7000F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; + BBDFDC5D2B2B8E2100F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */; }; CB24F70D29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */; }; CB6BCDF927C6BEFF00CC76DC /* PrivacyFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB6BCDF827C6BEFF00CC76DC /* PrivacyFeatures.swift */; }; @@ -2976,11 +2979,26 @@ EE7295EB2A545BFC008C0991 /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE7295EA2A545BFC008C0991 /* NetworkProtection */; }; EE7295ED2A545C0A008C0991 /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE7295EC2A545C0A008C0991 /* NetworkProtection */; }; EE7295EF2A545C12008C0991 /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE7295EE2A545C12008C0991 /* NetworkProtection */; }; + EEA3EEB12B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; + EEA3EEB32B24EC0600E8333A /* VPNLocationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */; }; EEAD7A7A2A1D3E20002A24E7 /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEAD7A7B2A1D3E20002A24E7 /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEC111E4294D06020086524F /* JSAlert.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EEC111E3294D06020086524F /* JSAlert.storyboard */; }; EEC111E6294D06290086524F /* JSAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC111E5294D06290086524F /* JSAlertViewModel.swift */; }; + EEC4A65E2B277E8D00F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; + EEC4A65F2B277EE100F7C0AA /* VPNLocationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */; }; + EEC4A6602B277F0D00F7C0AA /* VPNLocationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */; }; + EEC4A6612B277F1100F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; + EEC4A6692B2C87D300F7C0AA /* VPNLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */; }; + EEC4A66A2B2C87D300F7C0AA /* VPNLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */; }; + EEC4A66B2B2C87D300F7C0AA /* VPNLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */; }; + EEC4A66D2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */; }; + EEC4A66E2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */; }; + EEC4A66F2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */; }; + EEC4A6712B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */; }; + EEC4A6722B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */; }; + EEC4A6732B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */; }; EEC589D92A4F1CE300BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEC589DA2A4F1CE400BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; @@ -4217,6 +4235,7 @@ B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PrivacyDashboard.storyboard; sourceTree = ""; }; B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = ""; }; B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = ""; }; + BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionExternalWaitlistPixels.swift; sourceTree = ""; }; CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationURLProvider.swift; sourceTree = ""; }; CB6BCDF827C6BEFF00CC76DC /* PrivacyFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyFeatures.swift; sourceTree = ""; }; CBDD5DE229A67F2700832877 /* MockConfigurationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockConfigurationStore.swift; sourceTree = ""; }; @@ -4233,9 +4252,14 @@ EAE427FF275D47FA00DAC26B /* ClickToLoadModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClickToLoadModel.swift; sourceTree = ""; }; EAFAD6C92728BD1200F9DF00 /* clickToLoad.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = clickToLoad.js; sourceTree = ""; }; EE339227291BDEFD009F62C1 /* JSAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSAlertController.swift; sourceTree = ""; }; + EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; + EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLocationViewModel.swift; sourceTree = ""; }; EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppLauncher.swift; sourceTree = ""; }; EEC111E3294D06020086524F /* JSAlert.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = JSAlert.storyboard; sourceTree = ""; }; EEC111E5294D06290086524F /* JSAlertViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModel.swift; sourceTree = ""; }; + EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationView.swift; sourceTree = ""; }; + EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationPreferenceItemModel.swift; sourceTree = ""; }; + EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationPreferenceItem.swift; sourceTree = ""; }; EECE10E429DD77E60044D027 /* FeatureFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacPacketTunnelProvider.swift; sourceTree = ""; }; EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModelTests.swift; sourceTree = ""; }; @@ -4638,6 +4662,7 @@ 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureVisibility.swift */, 3199C6F82AF94F5B002A7BA1 /* DataBrokerProtectionFeatureDisabler.swift */, 3199C6FC2AF97367002A7BA1 /* DataBrokerProtectionAppEvents.swift */, + BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */, ); path = DBP; sourceTree = ""; @@ -5142,6 +5167,7 @@ 4B4D60632A0B29FA00BCD287 /* BothAppTargets */ = { isa = PBXGroup; children = ( + EEA3EEAF2B24EB5100E8333A /* VPNLocation */, 9D9AE8682AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift */, 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */, 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */, @@ -7893,6 +7919,18 @@ path = fonts; sourceTree = ""; }; + EEA3EEAF2B24EB5100E8333A /* VPNLocation */ = { + isa = PBXGroup; + children = ( + EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */, + EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */, + EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */, + EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */, + EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */, + ); + path = VPNLocation; + sourceTree = ""; + }; EEAEA3F4294D05CF00D04DF3 /* JSAlert */ = { isa = PBXGroup; children = ( @@ -9117,6 +9155,7 @@ 3706FA7F293F65D500E42796 /* TabIndex.swift in Sources */, 3706FA80293F65D500E42796 /* TabLazyLoaderDataSource.swift in Sources */, 3706FA81293F65D500E42796 /* LoginImport.swift in Sources */, + EEC4A66E2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 3706FA83293F65D500E42796 /* LazyLoadable.swift in Sources */, 3706FA84293F65D500E42796 /* ClickToLoadModel.swift in Sources */, 3706FA85293F65D500E42796 /* KeyedCodingExtension.swift in Sources */, @@ -9214,6 +9253,7 @@ 3706FADC293F65D500E42796 /* FirefoxLoginReader.swift in Sources */, 3706FADD293F65D500E42796 /* AtbParser.swift in Sources */, 3706FADE293F65D500E42796 /* PreferencesDuckPlayerView.swift in Sources */, + EEC4A65E2B277E8D00F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, B66260E729ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift in Sources */, 3706FADF293F65D500E42796 /* AddFolderModalViewController.swift in Sources */, 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */, @@ -9256,6 +9296,7 @@ 3706FAFD293F65D500E42796 /* DownloadsPopover.swift in Sources */, 3706FAFE293F65D500E42796 /* SpacerNode.swift in Sources */, 3706FB00293F65D500E42796 /* PasswordManagementCreditCardModel.swift in Sources */, + BBDFDC5D2B2B8E2100F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */, 3706FB01293F65D500E42796 /* NSEventExtension.swift in Sources */, 3706FB02293F65D500E42796 /* Onboarding.swift in Sources */, 4B9DB0482A983B24000927DB /* WaitlistRootView.swift in Sources */, @@ -9349,6 +9390,7 @@ 3706FB4D293F65D500E42796 /* GrammarFeaturesManager.swift in Sources */, 3706FB50293F65D500E42796 /* SafariFaviconsReader.swift in Sources */, 3706FB51293F65D500E42796 /* NSScreenExtension.swift in Sources */, + EEC4A66A2B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, 3706FB52293F65D500E42796 /* NSBezierPathExtension.swift in Sources */, 3706FB53293F65D500E42796 /* WebsiteDataStore.swift in Sources */, 3706FB54293F65D500E42796 /* PermissionContextMenu.swift in Sources */, @@ -9441,6 +9483,7 @@ 4B4032852AAAC24400CCA602 /* WaitlistActivationDateStore.swift in Sources */, 3706FB93293F65D500E42796 /* PasteboardFolder.swift in Sources */, 3706FB94293F65D500E42796 /* CookieManagedNotificationView.swift in Sources */, + EEC4A65F2B277EE100F7C0AA /* VPNLocationViewModel.swift in Sources */, 370A34B22AB24E3700C77F7C /* SyncDebugMenu.swift in Sources */, 4B4D60BE2A0C848A00BCD287 /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 3706FB95293F65D500E42796 /* PermissionType.swift in Sources */, @@ -9542,6 +9585,7 @@ 56D6A3D729DB2BAB0055215A /* ContinueSetUpView.swift in Sources */, B6BCC51F2AFCD9ED002C5499 /* DataImportSourcePicker.swift in Sources */, 3706FBDF293F65D500E42796 /* String+Punycode.swift in Sources */, + EEC4A6722B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 3706FBE0293F65D500E42796 /* NSException+Catch.m in Sources */, 3706FBE1293F65D500E42796 /* AppStateRestorationManager.swift in Sources */, 3706FBE2293F65D500E42796 /* ClickToLoadUserScript.swift in Sources */, @@ -10391,6 +10435,7 @@ B68D21D22ACBCA01002DA3C2 /* ContentBlockerRulesManagerMock.swift in Sources */, 4B9579F62AC7AE700062CA31 /* ChromiumFaviconsReader.swift in Sources */, 4B9579F72AC7AE700062CA31 /* SuggestionTableRowView.swift in Sources */, + EEC4A6732B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 4B9579F82AC7AE700062CA31 /* DownloadsPreferences.swift in Sources */, 4B9579F92AC7AE700062CA31 /* PasswordManagementItemList.swift in Sources */, 4B9579FA2AC7AE700062CA31 /* Bookmark.swift in Sources */, @@ -10558,6 +10603,7 @@ 4B957A8B2AC7AE700062CA31 /* PasswordManagementListSection.swift in Sources */, 4B957A8C2AC7AE700062CA31 /* FaviconReferenceCache.swift in Sources */, 4B957A8D2AC7AE700062CA31 /* BookmarkTreeController.swift in Sources */, + EEC4A6602B277F0D00F7C0AA /* VPNLocationViewModel.swift in Sources */, 4B957A8E2AC7AE700062CA31 /* FirefoxEncryptionKeyReader.swift in Sources */, 4B957A8F2AC7AE700062CA31 /* EventMapping+NetworkProtectionError.swift in Sources */, 4B957A902AC7AE700062CA31 /* BookmarkManagementSplitViewController.swift in Sources */, @@ -10617,6 +10663,7 @@ 4B957AC42AC7AE700062CA31 /* BWVault.swift in Sources */, 4B957AC52AC7AE700062CA31 /* NSViewExtension.swift in Sources */, 4B957AC62AC7AE700062CA31 /* Preferences.swift in Sources */, + BBDFDC5C2B2B8D7000F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */, 4B957AC72AC7AE700062CA31 /* DownloadListViewModel.swift in Sources */, 4B957AC82AC7AE700062CA31 /* BookmarkManagementDetailViewController.swift in Sources */, 4B957AC92AC7AE700062CA31 /* CSVImporter.swift in Sources */, @@ -10695,6 +10742,7 @@ 4B957B112AC7AE700062CA31 /* DateExtension.swift in Sources */, 4B957B122AC7AE700062CA31 /* History.xcdatamodeld in Sources */, 4B957B132AC7AE700062CA31 /* PermissionStore.swift in Sources */, + EEC4A6612B277F1100F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, 4B957B142AC7AE700062CA31 /* PrivacyIconViewModel.swift in Sources */, 4B957B152AC7AE700062CA31 /* ChromiumBookmarksReader.swift in Sources */, B66CA4212AD910B300447CF0 /* DataImportView.swift in Sources */, @@ -10736,6 +10784,7 @@ 4B957B382AC7AE700062CA31 /* LoadingProgressView.swift in Sources */, 7BEC20472B0F505F00243D3E /* BookmarkAddFolderPopoverViewController.swift in Sources */, 4B957B392AC7AE700062CA31 /* StatisticsStore.swift in Sources */, + EEC4A66B2B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, 4B957B3A2AC7AE700062CA31 /* BWInstallationService.swift in Sources */, 4B957B3B2AC7AE700062CA31 /* BookmarksBarPromptPopover.swift in Sources */, 4B957B3C2AC7AE700062CA31 /* NetworkProtectionInvitePresenter.swift in Sources */, @@ -10763,6 +10812,7 @@ 4B957B502AC7AE700062CA31 /* CoreDataStore.swift in Sources */, 4B957B512AC7AE700062CA31 /* BundleExtension.swift in Sources */, 4B957B522AC7AE700062CA31 /* NSOpenPanelExtensions.swift in Sources */, + EEC4A66F2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 4B957B532AC7AE700062CA31 /* FirePopover.swift in Sources */, 4B957B542AC7AE700062CA31 /* HistoryCoordinator.swift in Sources */, 4B957B552AC7AE700062CA31 /* NetworkProtectionOnboardingMenu.swift in Sources */, @@ -10962,6 +11012,7 @@ 85799C1825DEBB3F0007EC87 /* Logging.swift in Sources */, AAC30A2E268F1EE300D2D9CD /* CrashReportPromptPresenter.swift in Sources */, 1D2DC00629016798008083A1 /* BWCredential.swift in Sources */, + EEA3EEB12B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, 37AFCE8727DA334800471A10 /* PreferencesRootView.swift in Sources */, B684590825C9027900DC17B6 /* AppStateChangedPublisher.swift in Sources */, 4B92928F26670D1700AD2C21 /* BookmarkTableCellView.swift in Sources */, @@ -11064,6 +11115,7 @@ 37F19A6528E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift in Sources */, 4B9292D22667123700AD2C21 /* AddFolderModalViewController.swift in Sources */, 4B92929E26670D2A00AD2C21 /* BookmarkSidebarTreeController.swift in Sources */, + EEC4A6712B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 85589E8727BBB8F20038AD11 /* HomePageFavoritesModel.swift in Sources */, 4BB88B4A25B7B690006F6B06 /* SequenceExtensions.swift in Sources */, B602E7CF2A93A5FF00F12201 /* WKBackForwardListExtension.swift in Sources */, @@ -11110,6 +11162,7 @@ 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, 4B9DB01D2A983B24000927DB /* Waitlist.swift in Sources */, + BBDFDC5A2B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */, AAA0CC33252F181A0079BC96 /* NavigationButtonMenuDelegate.swift in Sources */, AAC30A2A268E239100D2D9CD /* CrashReport.swift in Sources */, 1D6A492029CF7A490011DF74 /* NSPopoverExtension.swift in Sources */, @@ -11156,6 +11209,7 @@ 4BBC16A027C4859400E00A38 /* DeviceAuthenticationService.swift in Sources */, CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */, 37CBCA9A2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, + EEC4A66D2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */, 3776582F27F82E62009A6B35 /* AutofillPreferences.swift in Sources */, AAD8078727B3F45600CF7703 /* WebsiteBreakage.swift in Sources */, 7BEC20422B0F505F00243D3E /* BookmarkAddPopoverViewController.swift in Sources */, @@ -11297,6 +11351,7 @@ B6DB3CFB26A17CB800D459B7 /* PermissionModel.swift in Sources */, 4B92929C26670D2A00AD2C21 /* PasteboardFolder.swift in Sources */, 3171D6B82889849F0068632A /* CookieManagedNotificationView.swift in Sources */, + EEA3EEB32B24EC0600E8333A /* VPNLocationViewModel.swift in Sources */, B6106BAB26A7BF1D0013B453 /* PermissionType.swift in Sources */, AAC6881B28626C1900D54247 /* RecentlyClosedWindow.swift in Sources */, 85707F2A276A35FE00DC0649 /* ActionSpeech.swift in Sources */, @@ -11514,6 +11569,7 @@ 4BA1A69B258B076900F6F690 /* FileStore.swift in Sources */, B6A9E47F26146A800067D1B9 /* PixelArguments.swift in Sources */, 37BF3F21286F0A7A00BD9014 /* PinnedTabsViewModel.swift in Sources */, + EEC4A6692B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, AAC5E4D225D6A709007F5990 /* BookmarkList.swift in Sources */, B602E81D2A1E25B1006D261F /* NEOnDemandRuleExtension.swift in Sources */, 4B9292D12667123700AD2C21 /* BookmarkTableRowView.swift in Sources */, @@ -12738,7 +12794,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 94.0.0; + version = 94.0.2; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9aaa137dac..ba0d558e52 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "e4f4ae624174c1398d345cfc387db38f8f69986d", - "version" : "94.0.0" + "revision" : "861b8a72930f138cd18b6a7722502a8a40375827", + "version" : "94.0.2" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "dbecae0df07650a21b5632a92fa2e498c96af7b5", - "version" : "10.0.1" + "revision" : "5597bc17709c8acf454ecaad4f4082007986242a", + "version" : "10.0.2" } }, { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 1905a1e3cc..96097271bd 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -303,10 +303,34 @@ struct UserText { // VPN Setting Titles + static let vpnLocationTitle = NSLocalizedString("vpn.location.title", value: "Location", comment: "Location section title in VPN settings") static let vpnGeneralTitle = NSLocalizedString("vpn.general.title", value: "General", comment: "General section title in VPN settings") static let vpnNotificationsSettingsTitle = NSLocalizedString("vpn.notifications.settings.title", value: "Notifications", comment: "Notifications section title in VPN settings") static let vpnAdvancedSettingsTitle = NSLocalizedString("vpn.advanced.settings.title", value: "Advanced", comment: "VPN Advanced section title in VPN settings") + // VPN Location + + static let vpnLocationChangeButtonTitle = NSLocalizedString("vpn.location.change.button.title", value: "Change...", comment: "Title of the VPN location preference change button") + static let vpnLocationListTitle = NSLocalizedString("vpn.location.list.title", value: "VPN Location", comment: "Title of the VPN location list screen") + static let vpnLocationRecommendedSectionTitle = NSLocalizedString("vpn.location.recommended.section.title", value: "Recommended", comment: "Title of the VPN location list recommended section") + static let vpnLocationCustomSectionTitle = NSLocalizedString("vpn.location.custom.section.title", value: "Custom", comment: "Title of the VPN location list custom section") + static let vpnLocationSubmitButtonTitle = NSLocalizedString("vpn.location.submit.button.title", value: "Submit", comment: "Title of the VPN location list submit button") + static let vpnLocationCancelButtonTitle = NSLocalizedString("vpn.location.custom.section.title", value: "Cancel", comment: "Title of the VPN location list cancel button") + static let vpnLocationNearest = NSLocalizedString( + "vpn.location.description.nearest", + value: "Nearest", + comment: "Nearest city setting description") + static let vpnLocationNearestAvailable = NSLocalizedString( + "vpn.location.description.nearest.available", + value: "Nearest Available", + comment: "Nearest available location setting description") + 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.") + + static func vpnLocationCountryItemFormattedCitiesCount(_ count: Int) -> String { + 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 : ") + return String(format: message, count) + } + // VPN Settings static let vpnConnectOnLoginSettingTitle = NSLocalizedString( @@ -373,8 +397,8 @@ struct UserText { static let autofillAddresses = NSLocalizedString("autofill.addresses", value: "Addresses", comment: "Autofill autosaved data type") static let autofillPaymentMethods = NSLocalizedString("autofill.payment-methods", value: "Payment methods", comment: "Autofill autosaved data type") static let autofillAutoLock = NSLocalizedString("autofill.auto-lock", value: "Auto-lock", comment: "Autofill settings section title") - static let autofillLockWhenIdle = NSLocalizedString("autofill.lock-when-idle", value: "Lock Autofill after computer is idle for", comment: "Autofill auto-lock setting") - static let autofillNeverLock = NSLocalizedString("autofill.never-lock", value: "Never lock Autofill", comment: "Autofill auto-lock setting") + static let autofillLockWhenIdle = NSLocalizedString("autofill.lock-when-idle", value: "Lock autofill after computer is idle for", comment: "Autofill auto-lock setting") + static let autofillNeverLock = NSLocalizedString("autofill.never-lock", value: "Never lock autofill", comment: "Autofill auto-lock setting") static let autofillNeverLockWarning = NSLocalizedString("autofill.never-lock-warning", value: "If not locked, anyone with access to your device will be able to use and modify your autofill data. For security purposes, credit card form fill always requires authentication.", comment: "Autofill disabled auto-lock warning") static let autolockLocksFormFill = NSLocalizedString("autofill.autolock-locks-form-filling", value: "Also lock password form fill", comment: "Lock form filling when auto-lock is active text") @@ -385,7 +409,7 @@ struct UserText { static let passwordManagement = NSLocalizedString("passsword.management", value: "Autofill", comment: "Used as title for password management user interface") static let passwordManagementAllItems = NSLocalizedString("passsword.management.all-items", value: "All Items", comment: "Used as title for the Autofill All Items option") - static let passwordManagementLogins = NSLocalizedString("passsword.management.logins", value: "Logins", comment: "Used as title for the Autofill Logins option") + static let passwordManagementLogins = NSLocalizedString("passsword.management.logins", value: "Passwords", comment: "Used as title for the Autofill Logins option") static let passwordManagementIdentities = NSLocalizedString("passsword.management.identities", value: "Identities", comment: "Used as title for the Autofill Identities option") static let passwordManagementCreditCards = NSLocalizedString("passsword.management.credit-cards", value: "Credit Cards", comment: "Used as title for the Autofill Credit Cards option") static let passwordManagementNotes = NSLocalizedString("passsword.management.notes", value: "Notes", comment: "Used as title for the Autofill Notes option") @@ -855,7 +879,7 @@ struct UserText { } static func passwordManagerAutosavePopoverText(domain: String) -> String { - let localized = NSLocalizedString("autofill.popover.autosave.text", value: "Login saved for %@", comment: "Text confirming a password has been saved for the %@ domain") + let localized = NSLocalizedString("autofill.popover.autosave.text", value: "Password saved for %@", comment: "Text confirming a password has been saved for the %@ domain") return String(format: localized, domain) } diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index b1ef218592..fb7b34b26e 100644 --- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"119cd38f776d198d0ecd736015643c24\"" - public static let embeddedDataSHA = "cc7c0549233b74a5afa469a358baecc9f56693d3684186ad6856fc4890c5cf96" + public static let embeddedDataETag = "\"ca66d409eb00e5c19f3a0abae449dd1a\"" + public static let embeddedDataSHA = "42f9d3064372bc85ac8ee37afe883ed4741d6a3cfcb9ce927c2f732c3f694140" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index d932539add..94914fa75a 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1702415315898, + "version": 1702579565498, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -1038,25 +1038,25 @@ "hash": "d757f6509e9a9a20140c755ed0f21ea2" }, "dbp": { - "state": "disabled", + "state": "enabled", "features": { "waitlistBetaActive": { - "state": "disabled" + "state": "enabled" }, "waitlist": { - "state": "disabled", + "state": "enabled", "rollout": { "steps": [ { - "percent": 1 + "percent": 3 } ] } } }, "exceptions": [], - "minSupportedVersion": "1.66.0", - "hash": "325d5463fd2f2b6c8b9ad9288ae6ceed" + "minSupportedVersion": "1.68.0", + "hash": "784ab72b62adf8b5c07f656f167a28d2" }, "duckPlayer": { "exceptions": [], @@ -1216,6 +1216,10 @@ "selector": ".ad-unit", "type": "hide-empty" }, + { + "selector": ".ad-unit-wrapper", + "type": "hide-empty" + }, { "selector": ".column-ad", "type": "hide-empty" @@ -1304,6 +1308,10 @@ "selector": ".ad-banner-container", "type": "hide-empty" }, + { + "selector": "#banner_ad", + "type": "hide-empty" + }, { "selector": "[class*='bannerAd']", "type": "hide-empty" @@ -1392,6 +1400,10 @@ "selector": "[id*='advert-']", "type": "hide-empty" }, + { + "selector": "[aria-label='advertisement']", + "type": "hide-empty" + }, { "selector": ".ads__inline", "type": "closest-empty" @@ -1640,6 +1652,19 @@ "upgrade to flickr pro to hide these ads" ], "domains": [ + { + "domain": "10minutemail.com", + "rules": [ + { + "selector": "#secondary_ads", + "type": "hide-empty" + }, + { + "selector": "#vi-smartbanner", + "type": "hide" + } + ] + }, { "domain": "3bmeteo.com", "rules": [ @@ -1656,6 +1681,27 @@ } ] }, + { + "domain": "9gag.com", + "rules": [ + { + "selector": ".billboard", + "type": "hide-empty" + }, + { + "selector": ".inline-ad-container", + "type": "hide-empty" + }, + { + "selector": ".salt-section", + "type": "hide-empty" + }, + { + "selector": "#top-adhesion", + "type": "hide-empty" + } + ] + }, { "domain": "abc.es", "rules": [ @@ -1825,6 +1871,19 @@ } ] }, + { + "domain": "businessinsider.com", + "rules": [ + { + "selector": ".in-post-sticky", + "type": "hide-empty" + }, + { + "selector": ".subnav-ad-layout", + "type": "hide-empty" + } + ] + }, { "domain": "carandclassic.com", "rules": [ @@ -1964,6 +2023,23 @@ } ] }, + { + "domain": "drugs.com", + "rules": [ + { + "selector": ".topbanner-wrap", + "type": "hide" + }, + { + "selector": ".display-ad-wrapper", + "type": "hide-empty" + }, + { + "selector": "[id*='ddc-sidebox-ad-stacked-wrap']", + "type": "hide-empty" + } + ] + }, { "domain": "ebay.com", "rules": [ @@ -2210,6 +2286,35 @@ } ] }, + { + "domain": "gbnews.com", + "rules": [ + { + "selector": ".video-inbody", + "type": "hide-empty" + }, + { + "selector": ".ad--billboard", + "type": "hide" + }, + { + "selector": ".ad--placeholder", + "type": "hide" + }, + { + "selector": ".stiky_sky", + "type": "hide" + }, + { + "selector": "[position='sticky_banner']", + "type": "hide" + }, + { + "selector": ".ad-inbody", + "type": "hide" + } + ] + }, { "domain": "getpocket.com", "rules": [ @@ -2426,6 +2531,10 @@ { "selector": ".in-post-sticky", "type": "hide-empty" + }, + { + "selector": ".subnav-ad-layout", + "type": "hide-empty" } ] }, @@ -2780,6 +2889,35 @@ } ] }, + { + "domain": "pcgamesn.com", + "rules": [ + { + "selector": ".static_mpu_wrap", + "type": "hide-empty" + }, + { + "selector": "#nn_astro_wrapper", + "type": "hide-empty" + }, + { + "selector": ".ad-nextpage", + "type": "hide" + }, + { + "selector": ".legion_primiswrapper", + "type": "hide-empty" + }, + { + "selector": ".nn_mobile_mpu_wrapper", + "type": "hide-empty" + }, + { + "selector": ".nn-sticky", + "type": "hide-empty" + } + ] + }, { "domain": "petapixel.com", "rules": [ @@ -3419,6 +3557,10 @@ { "selector": "[data-content='Advertisement']", "type": "hide-empty" + }, + { + "selector": "#YDC-Lead-Stack", + "type": "hide-empty" } ] }, @@ -3536,7 +3678,7 @@ ] }, "state": "enabled", - "hash": "b82b3912ad9e3fea5b8754fdec02bc86" + "hash": "c34713afaf78e090b587a923f132ed56" }, "exceptionHandler": { "exceptions": [ @@ -4614,6 +4756,16 @@ } ] }, + "appboycdn.com": { + "rules": [ + { + "rule": "js.appboycdn.com/web-sdk/3.1/appboy.min.js", + "domains": [ + "edx.org" + ] + } + ] + }, "aticdn.net": { "rules": [ { @@ -5491,6 +5643,7 @@ "domains": [ "doterra.com", "easyjet.com", + "edx.org", "worlddutyfree.com" ] }, @@ -5615,6 +5768,7 @@ { "rule": "pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", "domains": [ + "air-journal.fr", "arcadepunks.com", "daotranslate.com", "drakescans.com", @@ -7355,7 +7509,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "6d895ce49e17ece65475082c7e376325" + "hash": "5cecb6d28193f468b28b9afdaca04da1" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index f1ce0ade57..f462671ae7 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -63,13 +63,9 @@ struct DataBrokerProtectionAppEvents { if DataBrokerProtectionWaitlist().readyToAcceptTermsAndConditions { switch source { case .cardUI: - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistCardUITapped, - frequency: .dailyAndCount, - includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistCardUITapped, frequency: .dailyAndCount) case .localPush: - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistNotificationTapped, - frequency: .dailyAndCount, - includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistNotificationTapped, frequency: .dailyAndCount) } DataBrokerProtectionWaitlistViewControllerPresenter.show() @@ -82,9 +78,7 @@ struct DataBrokerProtectionAppEvents { private func sendActiveDataBrokerProtectionWaitlistUserPixel() { if DefaultDataBrokerProtectionFeatureVisibility().waitlistIsOngoing { - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistUserActive, - frequency: .dailyOnly, - includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistUserActive, frequency: .dailyOnly) } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionExternalWaitlistPixels.swift b/DuckDuckGo/DBP/DataBrokerProtectionExternalWaitlistPixels.swift new file mode 100644 index 0000000000..3ddbb6e016 --- /dev/null +++ b/DuckDuckGo/DBP/DataBrokerProtectionExternalWaitlistPixels.swift @@ -0,0 +1,50 @@ +// +// DataBrokerProtectionExternalWaitlistPixels.swift +// +// 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 + +struct DataBrokerProtectionExternalWaitlistPixels { + + static var isUserLocaleAllowed: Bool { + var regionCode: String? + if #available(macOS 13, *) { + regionCode = Locale.current.region?.identifier + } else { + regionCode = Locale.current.regionCode + } + +#if DEBUG // Always assume US for debug builds + regionCode = "US" +#endif + + return (regionCode ?? "US") == "US" + } + + static func fire(pixel: Pixel.Event, frequency: DailyPixel.PixelFrequency) { + if Self.isUserLocaleAllowed { + let isInternalUser = NSApp.delegateTyped.internalUserDecider.isInternalUser + DailyPixel.fire(pixel: pixel, + frequency: frequency, + includeAppVersionParameter: true, + withAdditionalParameters: [ + "isInternalUser": isInternalUser.description + ] + ) + } + } +} diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift index b0cc598478..6a86be92bc 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureVisibility.swift @@ -43,7 +43,7 @@ struct DefaultDataBrokerProtectionFeatureVisibility: DataBrokerProtectionFeature isWaitlistEnabled && isWaitlistBetaActive } - private var isUserLocaleAllowed: Bool { + var isUserLocaleAllowed: Bool { var regionCode: String? if #available(macOS 13, *) { regionCode = Locale.current.region?.identifier diff --git a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift index 8b784addef..66fba3ac63 100644 --- a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift @@ -25,6 +25,8 @@ public enum FeatureFlag: String { /// Add experimental atb parameter to SERP queries for internal users to display Privacy Reminder /// https://app.asana.com/0/1199230911884351/1205979030848528/f case appendAtbToSerpQueries + + case vpnGeoswitching } extension FeatureFlag: FeatureFlagSourceProviding { @@ -34,6 +36,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .internalOnly case .appendAtbToSerpQueries: return .internalOnly + case .vpnGeoswitching: + return .internalOnly } } } diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 1776e0aecc..f4d0faec88 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -340,7 +340,7 @@ final class MoreOptionsMenu: NSMenu { .withImage(NSImage(named: "DBP-Icon")) items.append(dataBrokerProtectionItem) - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistEntryPointMenuItemDisplayed, frequency: .dailyAndCount, includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistEntryPointMenuItemDisplayed, frequency: .dailyAndCount) } else { DefaultDataBrokerProtectionFeatureVisibility().disableAndDeleteForWaitlistUsers() diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift index 3186124aea..af341e35be 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -27,6 +27,8 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { case networkProtectionActiveUser case networkProtectionNewUser + case networkProtectionStartFailed + case networkProtectionEnableAttemptConnecting case networkProtectionEnableAttemptSuccess case networkProtectionEnableAttemptFailure @@ -74,7 +76,7 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { case networkProtectionRekeyCompleted - case networkProtectionSystemExtensionUnknownActivationResult + case networkProtectionSystemExtensionActivationFailure case networkProtectionUnhandledError(function: String, line: Int, error: Error) @@ -90,6 +92,9 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { case .networkProtectionNewUser: return "m_mac_netp_daily_active_u" + case .networkProtectionStartFailed: + return "m_mac_netp_start_failed" + case .networkProtectionEnableAttemptConnecting: return "m_mac_netp_ev_enable_attempt" @@ -201,8 +206,8 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { case .networkProtectionRekeyCompleted: return "m_mac_netp_rekey_completed" - case .networkProtectionSystemExtensionUnknownActivationResult: - return "m_mac_netp_system_extension_unknown_activation_result" + case .networkProtectionSystemExtensionActivationFailure: + return "m_mac_netp_system_extension_activation_failure" case .networkProtectionUnhandledError: return "m_mac_netp_unhandled_error" @@ -279,7 +284,7 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { .networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor, .networkProtectionWireguardErrorInvalidState, .networkProtectionWireguardErrorFailedDNSResolution, - .networkProtectionSystemExtensionUnknownActivationResult, + .networkProtectionSystemExtensionActivationFailure, .networkProtectionActiveUser, .networkProtectionNewUser, .networkProtectionEnableAttemptConnecting, @@ -288,7 +293,8 @@ enum NetworkProtectionPixelEvent: PixelKitEvent { .networkProtectionTunnelFailureDetected, .networkProtectionTunnelFailureRecovered, .networkProtectionLatency, - .networkProtectionLatencyError: + .networkProtectionLatencyError, + .networkProtectionStartFailed: return nil } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index bce34a70e0..3c9f1fda50 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -87,7 +87,7 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { statusObserver: ipcClient.connectionStatusObserver, serverInfoObserver: ipcClient.serverInfoObserver, connectionErrorObserver: ipcClient.connectionErrorObserver, - connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), + connectivityIssuesObserver: DisabledConnectivityIssueObserver(), controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() ) self.iconPublisher = NetworkProtectionIconPublisher(statusReporter: networkProtectionStatusReporter, iconProvider: iconProvider) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 815e8e55f6..2c4668f139 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -81,7 +81,13 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle // MARK: - Connection Status - private let statusTransitionAwaiter = ConnectionStatusTransitionAwaiter(statusObserver: ConnectionStatusObserverThroughSession(platformNotificationCenter: NSWorkspace.shared.notificationCenter, platformDidWakeNotification: NSWorkspace.didWakeNotification), transitionTimeout: .seconds(4)) + private let statusTransitionAwaiter = ConnectionStatusTransitionAwaiter( + statusObserver: ConnectionStatusObserverThroughSession( + platformNotificationCenter: NSWorkspace.shared.notificationCenter, + platformDidWakeNotification: NSWorkspace.didWakeNotification + ), + transitionTimeout: .seconds(10) + ) // MARK: - Tunnel Manager @@ -243,6 +249,7 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle // kill switch protocolConfiguration.enforceRoutes = settings.enforceRoutes + // this setting breaks Connection Tester protocolConfiguration.includeAllNetworks = settings.includeAllNetworks @@ -277,15 +284,14 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle } } - // MARK: - Ensure things are working + // MARK: - Activate System Extension #if NETP_SYSTEM_EXTENSION /// Ensures that the system extension is activated if necessary. /// private func activateSystemExtension(waitingForUserApproval: @escaping () -> Void) async throws { do { - try await networkExtensionController.activateSystemExtension( - waitingForUserApproval: waitingForUserApproval) + try await networkExtensionController.activateSystemExtension(waitingForUserApproval: waitingForUserApproval) } catch { switch error { case OSSystemExtensionError.requestSuperseded: @@ -294,18 +300,20 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle controllerErrorStore.lastErrorMessage = UserText.networkProtectionSystemSettings case SystemExtensionRequestError.unknownRequestResult: controllerErrorStore.lastErrorMessage = UserText.networkProtectionUnknownActivationError - - PixelKit.fire( - NetworkProtectionPixelEvent.networkProtectionSystemExtensionUnknownActivationResult, - frequency: .standard, - includeAppVersionParameter: true) case SystemExtensionRequestError.willActivateAfterReboot: controllerErrorStore.lastErrorMessage = UserText.networkProtectionPleaseReboot default: controllerErrorStore.lastErrorMessage = error.localizedDescription } - return + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure, + frequency: .standard, + withError: error, + includeAppVersionParameter: true + ) + + throw error } self.controllerErrorStore.lastErrorMessage = nil @@ -387,6 +395,10 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle try await start(tunnelManager) } } catch { + PixelKit.fire( + NetworkProtectionPixelEvent.networkProtectionStartFailed, frequency: .standard, withError: error, includeAppVersionParameter: true + ) + await stop() controllerErrorStore.lastErrorMessage = error.localizedDescription } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift new file mode 100644 index 0000000000..12bfce37da --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift @@ -0,0 +1,45 @@ +// +// NetworkProtectionVPNCountryLabelsModel.swift +// +// 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 + +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 + } +} + +#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift new file mode 100644 index 0000000000..ee139d7de2 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift @@ -0,0 +1,67 @@ +// +// NetworkProtectionVPNLocationPreferenceItem.swift +// +// 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 + +struct VPNLocationPreferenceItem: View { + let model: VPNLocationPreferenceItemModel + @State private var isShowingLocationSheet: Bool = false + + var body: some View { + VStack(alignment: .leading) { + HStack(spacing: 10) { + switch model.icon { + case .defaultIcon: + Image(systemName: "location.fill") + .resizable() + .frame(width: 18, height: 18) + case .emoji(let string): + Text(string).font(.system(size: 20)) + } + + VStack(alignment: .leading) { + Text(model.title) + .font(.system(size: 13)) + .foregroundColor(.primary) + if let subtitle = model.subtitle { + Text(subtitle) + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } + Spacer() + Button(UserText.vpnLocationChangeButtonTitle) { + isShowingLocationSheet = true + } + .sheet(isPresented: $isShowingLocationSheet) { + VPNLocationView(isPresented: $isShowingLocationSheet) + } + } + } + .frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .topLeading) + .padding(10) + .background(Color("BlackWhite1")) + .roundedBorder() + } + +} + +#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItemModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItemModel.swift new file mode 100644 index 0000000000..ea0e505b32 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItemModel.swift @@ -0,0 +1,49 @@ +// +// NetworkProtectionLocationSettingsItemModel.swift +// +// 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 + +struct VPNLocationPreferenceItemModel { + enum LocationIcon { + case defaultIcon + case emoji(String) + } + + let title: String + let subtitle: String? + let icon: LocationIcon + + init(selectedLocation: VPNSettings.SelectedLocation) { + switch selectedLocation { + case .nearest: + title = UserText.vpnLocationNearestAvailable + subtitle = UserText.vpnLocationNearestAvailableSubtitle + icon = .defaultIcon + case .location(let location): + let countryLabelsModel = NetworkProtectionVPNCountryLabelsModel(country: location.country) + title = countryLabelsModel.title + subtitle = selectedLocation.location?.city + icon = .emoji(countryLabelsModel.emoji) + } + } +} + +#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift new file mode 100644 index 0000000000..a19ecca45d --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift @@ -0,0 +1,245 @@ +// +// NetworkProtectionVPNLocationView.swift +// +// 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 SwiftUI +import SwiftUIExtensions + +struct VPNLocationView: View { + @StateObject var model = VPNLocationViewModel() + @Binding var isPresented: Bool + + var body: some View { + VStack(alignment: .leading) { + Text(UserText.vpnLocationListTitle) + .font(.system(size: 17, weight: .bold)) + .foregroundColor(.primary) + VStack(alignment: .leading, spacing: 16) { + nearest(isSelected: model.isNearestSelected) + countries() + } + .padding(0) + } + .padding(.horizontal, 56) + .padding(.top, 32) + .padding(.bottom, 20) + .frame(minWidth: 624, maxWidth: .infinity, minHeight: 514, maxHeight: 514, alignment: .top) + Spacer() + Group { + VPNLocationViewButtons( + onDone: { + model.onSubmit() + isPresented = false + }, onCancel: { + isPresented = false + }) + .navigationTitle(UserText.vpnFeedbackFormTitle) + .onAppear { + Task { + await model.onViewAppeared() + } + } + } + .background(Color.secondary.opacity(0.1)) + } + + @ViewBuilder + private func nearest(isSelected: Bool) -> some View { + PreferencePaneSection(vericalPadding: 12) { + Text(UserText.vpnLocationRecommendedSectionTitle) + .font(.system(size: 15)) + .foregroundColor(.primary) + ChecklistItem( + isSelected: isSelected, + action: { + Task { + await model.onNearestItemSelection() + } + }, label: { + Image(systemName: "location.fill") + .resizable() + .frame(width: 18, height: 18) + VStack(alignment: .leading, spacing: 4) { + Text(UserText.vpnLocationNearestAvailable) + .foregroundColor(.primary) + Text(UserText.vpnLocationNearestAvailableSubtitle) + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } + ) + .frame(idealWidth: .infinity, maxWidth: .infinity) + .padding(10) + .background(Color("BlackWhite1")) + .roundedBorder() + } + } + + @ViewBuilder + private func countries() -> some View { + switch model.state { + case .loading: + EmptyView() + .listRowBackground(Color.clear) + case .loaded(let countryItems): + PreferencePaneSection(vericalPadding: 12) { + Text(UserText.vpnLocationCustomSectionTitle) + .font(.system(size: 15)) + .foregroundColor(.primary) + LazyVStack(alignment: .leading) { + ForEach(countryItems) { item in + CountryItem( + itemModel: item, + action: { + Task { + await model.onCountryItemSelection(id: item.id) + } + }, cityPickerAction: { selection in + Task { + await model.onCountryItemSelection(id: item.id, cityId: selection) + } + }) + .padding(10) + } + } + .roundedBorder() + } + } + } +} + +private struct CountryItem: View { + let itemModel: VPNCountryItemModel + let action: () -> Void + let cityPickerAction: (String?) -> Void + + private var selectedCityItemBinding: Binding { + Binding { + itemModel.selectedCityItem + } set: { city in + cityPickerAction(city.id) + } + } + + init(itemModel: VPNCountryItemModel, action: @escaping () -> Void, cityPickerAction: @escaping (String?) -> Void) { + self.itemModel = itemModel + self.action = action + self.cityPickerAction = cityPickerAction + } + + var body: some View { + ChecklistItem( + isSelected: itemModel.isSelected, + action: action, + label: { + Text(itemModel.emoji) + VStack(alignment: .leading, spacing: 4) { + Text(itemModel.title) + .foregroundColor(.primary) + if let subtitle = itemModel.subtitle { + Text(subtitle) + .foregroundColor(.secondary) + } + } + if itemModel.shouldShowPicker { + Spacer() + Picker("", selection: selectedCityItemBinding) { + Text(itemModel.nearestCityPickerItem.name) + .tag(itemModel.nearestCityPickerItem) + Divider() + ForEach(itemModel.cityPickerItems) { cityItem in + Text(cityItem.name) + .tag(cityItem) + } + } + .pickerStyle(.menu) + .frame(width: 90) + } + } + ) + } +} + +private struct ChecklistItem: View where Content: View { + let isSelected: Bool + let action: () -> Void + @ViewBuilder let label: () -> Content + + var body: some View { + HStack(spacing: 12) { + Image(systemName: "checkmark") + .foregroundColor(Color.accentColor) + .if(!isSelected) { + $0.hidden() + } + label() + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .contentShape(Rectangle()) + .background(Color("BlackWhite1")) + .onTapGesture { + action() + } + } +} + +private struct VPNLocationViewButtons: View { + let onDone: () -> Void + let onCancel: () -> Void + + var body: some View { + HStack { + Spacer() + button(text: UserText.vpnLocationCancelButtonTitle, action: onCancel) + .keyboardShortcut(.cancelAction) + .buttonStyle(DismissActionButtonStyle()) + + button(text: UserText.vpnLocationSubmitButtonTitle, action: onDone) + .keyboardShortcut(.defaultAction) + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + } + .padding(.vertical, 16) + .padding(.horizontal, 20) + } + + @ViewBuilder + func button(text: String, action: @escaping () -> Void) -> some View { + Button(text) { + action() + } + } + +} + +extension View { + /// Applies the given transform if the given condition evaluates to `true`. + /// - Parameters: + /// - condition: The condition to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. + @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } +} + +#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift new file mode 100644 index 0000000000..9ad4aba68f --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift @@ -0,0 +1,185 @@ +// +// NetworkProtectionVPNLocationViewModel.swift +// +// 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 Combine +import NetworkProtection + +final class VPNLocationViewModel: ObservableObject { + private let locationListRepository: NetworkProtectionLocationListRepository + private let settings: VPNSettings + private var selectedLocation: VPNSettings.SelectedLocation + @Published public var state: LoadingState + @Published public var isNearestSelected: Bool + + enum ViewAction { + case cancel + case submit + } + + enum LoadingState { + case loading + case loaded(countryItems: [VPNCountryItemModel]) + + var isLoading: Bool { + switch self { + case .loading: + return true + case .loaded: + return false + } + } + } + + init(locationListRepository: NetworkProtectionLocationListRepository, settings: VPNSettings) { + self.locationListRepository = locationListRepository + self.settings = settings + state = .loading + selectedLocation = settings.selectedLocation + self.isNearestSelected = selectedLocation == .nearest + } + + func onViewAppeared() async { + await reloadList() + } + + func onNearestItemSelection() async { + selectedLocation = .nearest + await reloadList() + } + + func onCountryItemSelection(id: String, cityId: String? = nil) async { + let location = NetworkProtectionSelectedLocation(country: id, city: cityId) + selectedLocation = .location(location) + await reloadList() + } + + func onSubmit() { + settings.selectedLocation = selectedLocation + } + + @MainActor + private func reloadList() async { + guard let list = try? await locationListRepository.fetchLocationList() else { return } + let isNearestSelected = selectedLocation == .nearest + + let countryItems = list.map { currentLocation in + let isCountrySelected: Bool + var cityPickerItems: [CityItem] + let selectedCityItem: CityItem + + switch selectedLocation { + case .location(let location): + isCountrySelected = location.country == currentLocation.country + cityPickerItems = currentLocation.cities.map { currentCity in + let isCitySelected = currentCity.name == location.city + return CityItem(cityName: currentCity.name) + } + selectedCityItem = location.city.flatMap(CityItem.init(cityName:)) ?? .nearest + case .nearest: + isCountrySelected = false + cityPickerItems = currentLocation.cities.map { currentCity in + CityItem(cityName: currentCity.name) + } + selectedCityItem = .nearest + } + + return VPNCountryItemModel( + netPLocation: currentLocation, + isSelected: isCountrySelected, + cityPickerItems: cityPickerItems, + selectedCityItem: selectedCityItem + ) + } + self.isNearestSelected = isNearestSelected + state = .loaded(countryItems: countryItems) + } +} + +private typealias CountryItem = VPNCountryItemModel +private typealias CityItem = VPNCityItemModel + +struct VPNCountryItemModel: Identifiable { + private let labelsModel: NetworkProtectionVPNCountryLabelsModel + + var emoji: String { + labelsModel.emoji + } + var title: String { + labelsModel.title + } + let isSelected: Bool + var id: String + let subtitle: String? + let nearestCityPickerItem: VPNCityItemModel = .nearest + let cityPickerItems: [VPNCityItemModel] + let selectedCityItem: VPNCityItemModel + let shouldShowPicker: Bool + + fileprivate init(netPLocation: NetworkProtectionLocation, isSelected: Bool, cityPickerItems: [VPNCityItemModel], selectedCityItem: VPNCityItemModel) { + self.labelsModel = .init(country: netPLocation.country) + self.isSelected = isSelected + self.id = netPLocation.country + let hasMultipleCities = netPLocation.cities.count > 1 + self.subtitle = hasMultipleCities ? UserText.vpnLocationCountryItemFormattedCitiesCount(netPLocation.cities.count) : nil + self.cityPickerItems = cityPickerItems + self.shouldShowPicker = hasMultipleCities + self.selectedCityItem = selectedCityItem + } +} + +struct VPNCityItemModel: Identifiable, Hashable { + let id: String + let name: String + + fileprivate init(cityName: String) { + self.id = cityName + self.name = cityName + } +} + +extension VPNCityItemModel { + static var nearest: VPNCityItemModel { + Self.init(cityName: UserText.vpnLocationNearest) + } +} + +extension NetworkProtectionLocationListCompositeRepository { + convenience init() { + let settings = VPNSettings(defaults: .netP) + self.init( + environment: settings.selectedEnvironment, + tokenStore: NetworkProtectionKeychainTokenStore(), + errorEvents: .networkProtectionAppDebugEvents + ) + } +} + +extension VPNLocationViewModel { + convenience init() { + let locationListRepository = NetworkProtectionLocationListCompositeRepository() + self.init( + locationListRepository: locationListRepository, + settings: VPNSettings(defaults: .netP) + ) + } +} + +#endif diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index 453a505449..54c2a5420e 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -23,9 +23,13 @@ import Combine import Foundation import NetworkProtection import NetworkProtectionUI +import BrowserServicesKit final class VPNPreferencesModel: ObservableObject { + let shouldShowLocationItem: Bool + @Published var locationItem: VPNLocationPreferenceItemModel + @Published var alwaysON = true @Published var connectOnLogin: Bool { @@ -66,7 +70,8 @@ final class VPNPreferencesModel: ObservableObject { private var cancellables = Set() init(settings: VPNSettings = .init(defaults: .netP), - defaults: UserDefaults = .netP) { + defaults: UserDefaults = .netP, + featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger) { self.settings = settings connectOnLogin = settings.connectOnLogin @@ -75,8 +80,11 @@ final class VPNPreferencesModel: ObservableObject { showInMenuBar = settings.showInMenuBar showUninstallVPN = defaults.networkProtectionOnboardingStatus != .default onboardingStatus = defaults.networkProtectionOnboardingStatus + locationItem = VPNLocationPreferenceItemModel(selectedLocation: settings.selectedLocation) + shouldShowLocationItem = featureFlagger.isFeatureOn(.vpnGeoswitching) subscribeToOnboardingStatusChanges(defaults: defaults) + subscribeToLocationSettingChanges() } func subscribeToOnboardingStatusChanges(defaults: UserDefaults) { @@ -85,6 +93,13 @@ final class VPNPreferencesModel: ObservableObject { .store(in: &cancellables) } + func subscribeToLocationSettingChanges() { + settings.selectedLocationPublisher + .map(VPNLocationPreferenceItemModel.init(selectedLocation:)) + .assign(to: \.locationItem, onWeaklyHeld: self) + .store(in: &cancellables) + } + @MainActor func uninstallVPN() async { let response = await uninstallVPNConfirmationAlert().runModal() diff --git a/DuckDuckGo/Preferences/View/PreferencesVPNView.swift b/DuckDuckGo/Preferences/View/PreferencesVPNView.swift index b30d3b8358..7b16780933 100644 --- a/DuckDuckGo/Preferences/View/PreferencesVPNView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesVPNView.swift @@ -33,6 +33,13 @@ extension Preferences { TextMenuTitle(text: UserText.vpn) + if model.shouldShowLocationItem { + PreferencePaneSection { + TextMenuItemHeader(text: UserText.vpnLocationTitle) + VPNLocationPreferenceItem(model: model.locationItem) + } + } + // SECTION: Manage VPN PreferencePaneSection { diff --git a/DuckDuckGo/SecureVault/Extensions/NSAlert+PasswordManager.swift b/DuckDuckGo/SecureVault/Extensions/NSAlert+PasswordManager.swift index 7ed65191b9..a496e01b88 100644 --- a/DuckDuckGo/SecureVault/Extensions/NSAlert+PasswordManager.swift +++ b/DuckDuckGo/SecureVault/Extensions/NSAlert+PasswordManager.swift @@ -22,7 +22,7 @@ extension NSAlert { static func passwordManagerConfirmDeleteLogin() -> NSAlert { let alert = NSAlert() - alert.messageText = "Are you sure you want to delete this Login?" + alert.messageText = "Are you sure you want to delete this saved password?" alert.informativeText = "This action cannot be undone." alert.alertStyle = .warning alert.addButton(withTitle: "Delete") @@ -42,8 +42,8 @@ extension NSAlert { static func passwordManagerDuplicateLogin() -> NSAlert { let alert = NSAlert() - alert.messageText = "Duplicate login" - alert.informativeText = "You already have a login for this username and website." + alert.messageText = "Duplicate Password" + alert.informativeText = "You already have a password saved for this username and website." alert.alertStyle = .warning alert.addButton(withTitle: "OK") return alert @@ -51,7 +51,7 @@ extension NSAlert { static func passwordManagerConfirmDeleteCard() -> NSAlert { let alert = NSAlert() - alert.messageText = "Are you sure you want to delete this Payment Method from Autofill?" + alert.messageText = "Are you sure you want to delete this saved credit card?" alert.informativeText = "This action cannot be undone." alert.alertStyle = .warning alert.addButton(withTitle: "Delete") @@ -61,7 +61,7 @@ extension NSAlert { static func passwordManagerConfirmDeleteIdentity() -> NSAlert { let alert = NSAlert() - alert.messageText = "Are you sure you want to delete this Info from Autofill?" + alert.messageText = "Are you sure you want to delete this saved autofill info?" alert.informativeText = "This action cannot be undone." alert.alertStyle = .warning alert.addButton(withTitle: "Delete") diff --git a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift index 33707e199c..23de1bdfa9 100644 --- a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift +++ b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift @@ -20,21 +20,21 @@ import Foundation extension UserText { - static let pmSaveCredentialsEditableTitle = NSLocalizedString("pm.save-credentials.editable.title", value: "Save Login?", comment: "Title for the editable Save Credentials popover") - static let pmSaveCredentialsNonEditableTitle = NSLocalizedString("pm.save-credentials.non-editable.title", value: "New Login Saved", comment: "Title for the non-editable Save Credentials popover") + static let pmSaveCredentialsEditableTitle = NSLocalizedString("pm.save-credentials.editable.title", value: "Save password?", comment: "Title for the editable Save Credentials popover") + static let pmSaveCredentialsNonEditableTitle = NSLocalizedString("pm.save-credentials.non-editable.title", value: "New Password Saved", comment: "Title for the non-editable Save Credentials popover") - static let pmEmptyStateDefaultTitle = NSLocalizedString("pm.empty.default.title", value: "No Logins or Payment Methods saved yet", comment: "Label for default empty state title") + static let pmEmptyStateDefaultTitle = NSLocalizedString("pm.empty.default.title", value: "No passwords or credit cards saved yet", comment: "Label for default empty state title") static let pmEmptyStateDefaultDescription = NSLocalizedString("pm.empty.default.description", - value: "If your logins are saved in another browser, you can import them into DuckDuckGo.", + value: "If your passwords are saved in another browser, you can import them into DuckDuckGo.", comment: "Label for default empty state description") - static let pmEmptyStateLoginsTitle = NSLocalizedString("pm.empty.logins.title", value: "No Logins", comment: "Label for logins empty state title") + static let pmEmptyStateLoginsTitle = NSLocalizedString("pm.empty.logins.title", value: "No passwords", comment: "Label for logins empty state title") static let pmEmptyStateIdentitiesTitle = NSLocalizedString("pm.empty.identities.title", value: "No Identities", comment: "Label for identities empty state title") static let pmEmptyStateCardsTitle = NSLocalizedString("pm.empty.cards.title", value: "No Cards", comment: "Label for cards empty state title") static let pmEmptyStateNotesTitle = NSLocalizedString("pm.empty.notes.title", value: "No Notes", comment: "Label for notes empty state title") static let pmNewCard = NSLocalizedString("pm.new.card", value: "Credit Card", comment: "Label for new card title") - static let pmNewLogin = NSLocalizedString("pm.new.login", value: "Login", comment: "Label for new login title") + static let pmNewLogin = NSLocalizedString("pm.new.login", value: "Password", comment: "Label for new login title") static let pmNewIdentity = NSLocalizedString("pm.new.identity", value: "Identity", comment: "Label for new identity title") static let pmNewNote = NSLocalizedString("pm.new.note", value: "Note", comment: "Label for new note title") @@ -102,7 +102,7 @@ extension UserText { static func pmLockScreenDuration(duration: String) -> String { let localized = NSLocalizedString("pm.lock-screen.duration", - value: "Your Autofill info will remain unlocked until your computer is idle for %@.", + value: "Your autofill info will remain unlocked until your computer is idle for %@.", comment: "") return String(format: localized, duration) } @@ -110,10 +110,10 @@ extension UserText { static let pmLockScreenPreferencesLabel = NSLocalizedString("pm.lock-screen.preferences.label", value: "Change in", comment: "Label used for a button that opens preferences") static let pmLockScreenPreferencesLink = NSLocalizedString("pm.lock-screen.preferences.link", value: "Settings", comment: "Label used for a button that opens preferences") - static let pmAutoLockPromptUnlockLogins = NSLocalizedString("pm.lock-screen.prompt.unlock-logins", value: "unlock access to your Autofill info", comment: "Label presented when unlocking Autofill") + static let pmAutoLockPromptUnlockLogins = NSLocalizedString("pm.lock-screen.prompt.unlock-logins", value: "unlock access to your autofill info", comment: "Label presented when unlocking Autofill") static let pmAutoLockPromptExportLogins = NSLocalizedString("pm.lock-screen.prompt.export-logins", value: "export your usernames and passwords", comment: "Label presented when exporting logins") - static let pmAutoLockPromptChangeLoginsSettings = NSLocalizedString("pm.lock-screen.prompt.change-settings", value: "change your Autofill info access settings", comment: "Label presented when changing Auto-Lock settings") - static let pmAutoLockPromptAutofill = NSLocalizedString("pm.lock-screen.prompt.autofill", value: "unlock access to your Autofill info", comment: "Label presented when autofilling credit card information") + static let pmAutoLockPromptChangeLoginsSettings = NSLocalizedString("pm.lock-screen.prompt.change-settings", value: "change your autofill info access settings", comment: "Label presented when changing Auto-Lock settings") + static let pmAutoLockPromptAutofill = NSLocalizedString("pm.lock-screen.prompt.autofill", value: "unlock access to your autofill info", comment: "Label presented when autofilling credit card information") static let autoLockThreshold1Minute = NSLocalizedString("pm.lock-screen.threshold.1-minute", value: "1 minute", comment: "Label used when selecting the Auto-Lock threshold") static let autoLockThreshold5Minutes = NSLocalizedString("pm.lock-screen.threshold.5-minutes", value: "5 minutes", comment: "Label used when selecting the Auto-Lock threshold") diff --git a/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift b/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift index a2354bedb7..492bcec5e6 100644 --- a/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift +++ b/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift @@ -437,7 +437,7 @@ final class PasswordManagementItemListModel: ObservableObject { var sections = [PasswordManagementListSection]() - if !accounts.isEmpty { sections.append(PasswordManagementListSection(title: "Logins", items: accounts)) } + if !accounts.isEmpty { sections.append(PasswordManagementListSection(title: "Passwords", items: accounts)) } if !cards.isEmpty { sections.append(PasswordManagementListSection(title: "Credit Cards", items: cards)) } if !identities.isEmpty { sections.append(PasswordManagementListSection(title: "Identities", items: identities)) } if !notes.isEmpty { sections.append(PasswordManagementListSection(title: "Notes", items: notes)) } diff --git a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard index 81af455c65..157d696f24 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManager.storyboard +++ b/DuckDuckGo/SecureVault/View/PasswordManager.storyboard @@ -577,7 +577,7 @@ - + diff --git a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift index afee252fc8..72beee02c1 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift @@ -93,7 +93,7 @@ struct DataBrokerProtectionWaitlistViewControllerPresenter: WaitlistViewControll guard let windowController = WindowControllersManager.shared.lastKeyMainWindowController else { return } - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistIntroDisplayed, frequency: .dailyAndCount, includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistIntroDisplayed, frequency: .dailyAndCount) // This is a hack to get around an issue with the waitlist notification screen showing the wrong state while it animates in, and then // jumping to the correct state as soon as the animation is complete. This works around that problem by providing the correct state up front, diff --git a/DuckDuckGo/Waitlist/Waitlist.swift b/DuckDuckGo/Waitlist/Waitlist.swift index 5dfc86eb2b..6400f655a0 100644 --- a/DuckDuckGo/Waitlist/Waitlist.swift +++ b/DuckDuckGo/Waitlist/Waitlist.swift @@ -357,9 +357,7 @@ struct DataBrokerProtectionWaitlist: Waitlist { UserDefaults().setValue(true, forKey: UserDefaultsWrapper.Key.shouldShowDBPWaitlistInvitedCardUI.rawValue) sendInviteCodeAvailableNotification { - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistNotificationShown, - frequency: .dailyAndCount, - includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistNotificationShown, frequency: .dailyAndCount) } } } diff --git a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift index 654f877029..df897b2ab6 100644 --- a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift +++ b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift @@ -57,7 +57,7 @@ struct DataBrokerProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTerm var acceptedTermsAndConditions: Bool func didShow() { - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistTermsAndConditionsDisplayed, frequency: .dailyAndCount, includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistTermsAndConditionsDisplayed, frequency: .dailyAndCount) } mutating func didAccept() { @@ -65,7 +65,7 @@ struct DataBrokerProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTerm // Remove delivered NetP notifications in case the user didn't click them. UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [DataBrokerProtectionWaitlist.notificationIdentifier]) - DailyPixel.fire(pixel: .dataBrokerProtectionWaitlistTermsAndConditionsAccepted, frequency: .dailyAndCount, includeAppVersionParameter: true) + DataBrokerProtectionExternalWaitlistPixels.fire(pixel: .dataBrokerProtectionWaitlistTermsAndConditionsAccepted, frequency: .dailyAndCount) } } diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index a615e1e0fa..bc3af58b03 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -105,7 +105,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { statusObserver: statusObserver, serverInfoObserver: serverInfoObserver, connectionErrorObserver: errorObserver, - connectivityIssuesObserver: ConnectivityIssueObserverThroughDistributedNotifications(), + connectivityIssuesObserver: DisabledConnectivityIssueObserver(), controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() ) }() diff --git a/LocalPackages/Account/Package.swift b/LocalPackages/Account/Package.swift index 90199085d0..0e357ade93 100644 --- a/LocalPackages/Account/Package.swift +++ b/LocalPackages/Account/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["Account"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.2"), .package(path: "../Purchase") ], targets: [ diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index dd215d3009..d5c101fa40 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.2"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift index c579c9ac55..a3cbcf36f3 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateCalculatorTests.swift @@ -476,7 +476,7 @@ final class OperationPreferredDateCalculatorTests: XCTestCase { /* If the time elapsed since the last profile removal exceeds the current date plus maintenance period (expired), we should proceed with scheduling a new opt-out request as the broker has failed to honor the previous one. */ - func testMatchFoundWithExpiredProfileWithRecentDate_thenOptOutDateDoesNotChange() throws { + func skipMatchFoundWithExpiredProfileWithRecentDate_thenOptOutDateDoesNotChange() throws { let expiredDate = Date().addingTimeInterval(-schedulingConfig.maintenanceScan.hoursToSeconds) let expectedOptOutDate = Date() diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 7c71ce91bd..99ead44dd0 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "94.0.2"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions") ], diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift index 8f2ca1bd8d..cbb4562111 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift @@ -27,10 +27,12 @@ public extension PixelKit { public static let osMajorVersion = "osMajorVersion" public static let errorCode = "e" + public static let errorDomain = "errorDomain" public static let errorDesc = "d" public static let errorCount = "c" public static let errorSource = "error_source" public static let underlyingErrorCode = "ue" + public static let underlyingErrorDomain = "underlyingErrorDomain" public static let underlyingErrorDesc = "ud" public static let underlyingErrorSQLiteCode = "sqlrc" public static let underlyingErrorSQLiteExtendedCode = "sqlerc" diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift index 786c77dc5a..f16a51d9db 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift @@ -121,16 +121,23 @@ public final class PixelKit { private func fire(pixelNamed pixelName: String, frequency: Frequency, - withHeaders headers: [String: String]? = nil, - withAdditionalParameters params: [String: String]? = nil, - allowedQueryReservedCharacters: CharacterSet? = nil, - includeAppVersionParameter: Bool = true, - onComplete: @escaping CompletionBlock = { _, _ in }) { + withHeaders headers: [String: String]?, + withAdditionalParameters params: [String: String]?, + withError error: Error?, + allowedQueryReservedCharacters: CharacterSet?, + includeAppVersionParameter: Bool, + onComplete: @escaping CompletionBlock) { var newParams = params ?? [:] + if includeAppVersionParameter { newParams[Parameters.appVersion] = appVersion } + + if let error { + newParams.appendErrorPixelParams(error: error) + } + #if DEBUG newParams[Parameters.test] = Values.test #endif @@ -203,6 +210,7 @@ public final class PixelKit { frequency: Frequency = .standard, withHeaders headers: [String: String]? = nil, withAdditionalParameters params: [String: String]? = nil, + withError error: Error? = nil, allowedQueryReservedCharacters: CharacterSet? = nil, includeAppVersionParameter: Bool = true, onComplete: @escaping CompletionBlock = { _, _ in }) { @@ -235,6 +243,7 @@ public final class PixelKit { frequency: frequency, withHeaders: headers, withAdditionalParameters: newParams, + withError: error, allowedQueryReservedCharacters: allowedQueryReservedCharacters, includeAppVersionParameter: includeAppVersionParameter, onComplete: onComplete) @@ -244,6 +253,7 @@ public final class PixelKit { frequency: Frequency = .standard, withHeaders headers: [String: String] = [:], withAdditionalParameters parameters: [String: String]? = nil, + withError error: Error? = nil, allowedQueryReservedCharacters: CharacterSet? = nil, includeAppVersionParameter: Bool = true, onComplete: @escaping CompletionBlock = { _, _ in }) { @@ -252,6 +262,7 @@ public final class PixelKit { frequency: frequency, withHeaders: headers, withAdditionalParameters: parameters, + withError: error, allowedQueryReservedCharacters: allowedQueryReservedCharacters, includeAppVersionParameter: includeAppVersionParameter, onComplete: onComplete) @@ -310,3 +321,22 @@ public final class PixelKit { } } + +extension Dictionary where Key == String, Value == String { + + mutating func appendErrorPixelParams(error: Error) { + let nsError = error as NSError + + self[PixelKit.Parameters.errorCode] = "\(nsError.code)" + self[PixelKit.Parameters.errorDomain] = nsError.domain + + if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { + self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" + self[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain + } else if let sqlErrorCode = nsError.userInfo["NSSQLiteErrorDomain"] as? NSNumber { + self[PixelKit.Parameters.underlyingErrorCode] = "\(sqlErrorCode.intValue)" + self[PixelKit.Parameters.underlyingErrorDomain] = "NSSQLiteErrorDomain" + } + } + +} diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift index fe4f6c3fef..928aa1123f 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift @@ -143,7 +143,7 @@ enum UserText { static let bookmarksLimitExceededDescription = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-description", value: "Bookmark limit exceeded. Delete some to resume syncing.", comment: "Description for sync bookmarks limits exceeded warning") static let credentialsLimitExceededDescription = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-description", value: "Logins limit exceeded. Delete some to resume syncing.", comment: "Description for sync credentials limits exceeded warning") static let bookmarksLimitExceededAction = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-action", value: "Manage Bookmarks", comment: "Button title for sync bookmarks limits exceeded warning to manage bookmarks") - static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", value: "Manage Logins", comment: "Button title for sync credentials limits exceeded warning to manage logins") + static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", value: "Manage passwords...", comment: "Button title for sync credentials limits exceeded warning to manage logins") static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", value: "Sync Error", comment: "Title for sync error alert") static let unableToSyncDescription = NSLocalizedString("alert.unable-to-sync-description", value: "Unable to sync.", comment: "Description for unable to sync error") static let unableToGetDevicesDescription = NSLocalizedString("alert.unable-to-get-devices-description", value: "Unable to retrieve the list of connected devices.", comment: "Description for unable to get devices error")