From 2ff496cc62982263656a1ee9742b5b8db5366f75 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 21 Feb 2024 11:18:40 +0100 Subject: [PATCH 01/12] Fix S3 access secrets --- .github/workflows/publish_dmg_release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml index 86c787cb1d..8ccbe6905b 100644 --- a/.github/workflows/publish_dmg_release.yml +++ b/.github/workflows/publish_dmg_release.yml @@ -109,8 +109,8 @@ jobs: - name: Upload to S3 id: upload env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }} AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} run: | # Back up existing appcast2.xml From 60c5f366ba3978e7b975a9a4a422781c0673a79a Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 21 Feb 2024 11:46:56 +0100 Subject: [PATCH 02/12] Update upload_to_s3.sh to use curl for checking files existence on CDN --- scripts/upload_to_s3/upload_to_s3.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/upload_to_s3/upload_to_s3.sh b/scripts/upload_to_s3/upload_to_s3.sh index ac7979f4c2..ddce4555c6 100755 --- a/scripts/upload_to_s3/upload_to_s3.sh +++ b/scripts/upload_to_s3/upload_to_s3.sh @@ -2,6 +2,7 @@ # Constants S3_PATH="s3://ddg-staticcdn/macos-desktop-browser/" +CDN_PATH="https://staticcdn.duckduckgo.com/macos-desktop-browser/" # Defaults if [[ -n "$CI" ]]; then @@ -149,13 +150,13 @@ for FILENAME in $FILES_TO_UPLOAD; do fi # Check if the file exists on S3 - AWS_CMD="$AWS s3 ls ${S3_PATH}${FILENAME}" - echo "Checking S3 for ${S3_PATH}${FILENAME}..." - if ! $AWS s3 ls "${S3_PATH}${FILENAME}" > /dev/null 2>&1; then - echo "$FILENAME not found on S3. Marking for upload." - MISSING_FILES+=("$FILENAME") + printf '%s' "Checking CDN for ${CDN_PATH}${FILENAME} ... " + if curl -fLSsI "${CDN_PATH}${FILENAME}" >/dev/null 2>&1; then + echo "✅" else - echo "$FILENAME exists on S3. Skipping." + echo "❌" + echo "🚢 Marking $FILENAME for upload." + MISSING_FILES+=("$FILENAME") fi done From 0ae321b117f224ae71a9e10cabe64ce88f4828a6 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 21 Feb 2024 18:28:57 +0100 Subject: [PATCH 03/12] Fix refreshing of subscription preference pane when switching between panes (#2230) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206654425458681/f Tech Design URL: CC: **Description**: Fix refreshing of subscription preference pane when switching between panes. Also the task var was not nilled so it was only fired once per viewModel instance. Additionally the check for the expiry date of subscription was wrong and it preemptively cleared cached entitlements resulting in blinking and refresh for every settings pane switch. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../PreferencesSubscriptionModel.swift | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index 15b6c3ba93..22ab992e79 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -173,14 +173,18 @@ public final class PreferencesSubscriptionModel: ObservableObject { func fetchAndUpdateSubscriptionDetails() { guard fetchSubscriptionDetailsTask == nil else { return } - fetchSubscriptionDetailsTask = Task { - guard let token = accountManager.accessToken else { return } + fetchSubscriptionDetailsTask = Task { [weak self] in + defer { + self?.fetchSubscriptionDetailsTask = nil + } + + guard let token = self?.accountManager.accessToken else { return } if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { - updateDescription(for: cachedDate) + self?.updateDescription(for: cachedDate) - if cachedDate.timeIntervalSinceNow > 0 { - self.cachedEntitlements = [] + if cachedDate.timeIntervalSinceNow < 0 { + self?.cachedEntitlements = [] } } @@ -190,13 +194,13 @@ public final class PreferencesSubscriptionModel: ObservableObject { return } - updateDescription(for: response.expiresOrRenewsAt) + self?.updateDescription(for: response.expiresOrRenewsAt) - subscriptionPlatform = response.platform + self?.subscriptionPlatform = response.platform } if case let .success(entitlements) = await AccountManager().fetchEntitlements() { - self.cachedEntitlements = entitlements + self?.cachedEntitlements = entitlements } } } From 5e2a8c0c225cc11787b8422c015b05cf292ee464 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Wed, 21 Feb 2024 18:39:18 +0000 Subject: [PATCH 04/12] DBP API changes for WebUI - Release (#2233) Cherry picked from https://github.com/duckduckgo/macos-browser/pull/2146 Task/Issue URL: https://app.asana.com/0/1204167627774280/1206480273655878/f **Description**: Add url property to data broker model, and changes result for WebUI call to support new UI designs --- .../DataBrokerProtectionDataManager.swift | 22 +++ .../DataBrokerDatabaseBrowserView.swift | 12 +- .../Model/DBPUICommunicationModel.swift | 17 ++ .../Model/DataBroker.swift | 51 +++++ .../Operations/DataBrokerOperation.swift | 2 +- .../DataBrokerProtectionBrokerUpdater.swift | 2 +- .../JSON/advancedbackgroundchecks.com.json | 11 +- .../Resources/JSON/backgroundcheck.run.json | 7 +- .../Resources/JSON/centeda.com.json | 13 +- .../Resources/JSON/clubset.com.json | 9 +- .../Resources/JSON/clustrmaps.com.json | 13 +- .../Resources/JSON/councilon.com.json | 9 +- .../Resources/JSON/curadvisor.com.json | 11 +- .../JSON/cyberbackgroundchecks.com.json | 9 +- .../Resources/JSON/dataveria.com.json | 13 +- .../JSON/fastbackgroundcheck.com.json | 11 +- .../JSON/freepeopledirectory.com.json | 9 +- .../Resources/JSON/inforver.com.json | 13 +- .../Resources/JSON/kwold.com.json | 9 +- .../Resources/JSON/neighbor.report.json | 25 +-- .../Resources/JSON/newenglandfacts.com.json | 9 +- .../Resources/JSON/officialusa.com.json | 11 +- .../JSON/people-background-check.com.json | 9 +- .../Resources/JSON/peoplefinders.com.json | 35 ++-- .../Resources/JSON/peoplesearchnow.com.json | 9 +- .../Resources/JSON/pub360.com.json | 13 +- .../Resources/JSON/publicreports.com.json | 9 +- .../Resources/JSON/quickpeopletrace.com.json | 12 +- .../Resources/JSON/searchpeoplefree.com.json | 9 +- .../JSON/smartbackgroundchecks.com.json | 9 +- .../Resources/JSON/spokeo.com.json | 55 ++++-- .../Resources/JSON/truepeoplesearch.com.json | 9 +- .../Resources/JSON/usa-people-search.com.json | 13 +- .../Resources/JSON/usatrace.com.json | 9 +- .../Resources/JSON/usphonebook.com.json | 13 +- .../Resources/JSON/verecor.com.json | 29 +-- .../Resources/JSON/vericora.com.json | 13 +- .../Resources/JSON/veriforia.com.json | 13 +- .../Resources/JSON/veripages.com.json | 13 +- .../Resources/JSON/virtory.com.json | 9 +- .../Resources/JSON/wellnut.com.json | 9 +- .../DataBrokerProtectionProcessor.swift | 1 - .../Services/EmailService.swift | 6 +- ...DataBrokerProtectionDatabaseProvider.swift | 18 +- .../Storage/Mappers.swift | 3 +- .../Storage/SchedulerSchema.swift | 4 + .../UI/DBPUICommunicationLayer.swift | 10 +- .../DataBrokerProtection/UI/UIMapper.swift | 80 ++++++-- .../BrokerJSONCodableTests.swift | 187 +++++++++++++++++- ...kerProfileQueryOperationManagerTests.swift | 13 +- .../DataBrokerProtectionUpdaterTests.swift | 6 +- .../EmailServiceTests.swift | 6 +- .../MapperToUITests.swift | 54 ++--- .../MismatchCalculatorUseCaseTests.swift | 2 + .../DataBrokerProtectionTests/Mocks.swift | 8 +- .../OperationPreferredDateUpdaterTests.swift | 1 + 56 files changed, 691 insertions(+), 286 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index 857f174360..52eabbae92 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -290,4 +290,26 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { return mapper.maintenanceScanState(brokerProfileQueryData) } + + func getDataBrokers() async -> [DBPUIDataBroker] { + brokerProfileQueryData + // 1. We get all brokers (in this list brokers are repeated) + .map { $0.dataBroker } + // 2. We map the brokers to the UI model + .flatMap { dataBroker -> [DBPUIDataBroker] in + var result: [DBPUIDataBroker] = [] + result.append(DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url)) + + for mirrorSite in dataBroker.mirrorSites { + result.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url)) + } + return result + } + // 3. We delete duplicates + .reduce(into: [DBPUIDataBroker]()) { (result, dataBroker) in + if !result.contains(where: { $0.url == dataBroker.url }) { + result.append(dataBroker) + } + } + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift index 18b5ffa6b6..fd09e77417 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift @@ -49,6 +49,7 @@ struct DatabaseView: View { @State private var isPopoverVisible = false @State private var selectedData: String = "" let data: [DataBrokerDatabaseBrowserData.Row] + let rowHeight: CGFloat = 40.0 var body: some View { if data.count > 0 { @@ -62,6 +63,11 @@ struct DatabaseView: View { } } + private func spacerHeight(_ geometry: GeometryProxy) -> CGFloat { + let result = geometry.size.height - CGFloat(data.count) * rowHeight + return max(0, result) + } + private func dataView() -> some View { GeometryReader { geometry in ScrollView([.horizontal, .vertical]) { @@ -86,7 +92,8 @@ struct DatabaseView: View { ForEach(row.data.keys.sorted(), id: \.self) { key in VStack { Text("\(row.data[key]?.description ?? "")") - .frame(maxWidth: 200, maxHeight: 50) + .frame(maxWidth: 200) + .frame(height: rowHeight) .frame(minWidth: 60) .onTapGesture { selectedData = row.data[key]?.description ?? "" @@ -100,7 +107,8 @@ struct DatabaseView: View { } } } - Spacer(minLength: geometry.size.height) + Spacer() + .frame(height: spacerHeight(geometry)) } .frame(minWidth: geometry.size.width, minHeight: 0, alignment: .topLeading) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index 353e3912df..5f242b34a2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -103,12 +103,24 @@ struct DBPUIAddressAtIndex: Codable { /// Message Object representing a data broker struct DBPUIDataBroker: Codable, Hashable { let name: String + let url: String + let date: Double? + + init(name: String, url: String, date: Double? = nil) { + self.name = name + self.url = url + self.date = date + } func hash(into hasher: inout Hasher) { hasher.combine(name) } } +struct DBPUIDataBrokerList: DBPUISendableMessage { + let dataBrokers: [DBPUIDataBroker] +} + /// Message Object representing a requested change to the user profile's brith year struct DBPUIBirthYear: Codable { let year: Int @@ -123,6 +135,7 @@ struct DBPUIDataBrokerProfileMatch: Codable { let addresses: [DBPUIUserProfileAddress] let alternativeNames: [String] let relatives: [String] + let date: Double? // Used in some methods to set the removedDate or found date } /// Protocol to represent a message that can be passed from the host to the UI @@ -139,6 +152,10 @@ struct DBPUIScanAndOptOutMaintenanceState: DBPUISendableMessage { struct DBPUIOptOutMatch: DBPUISendableMessage { let dataBroker: DBPUIDataBroker let matches: Int + let name: String + let alternativeNames: [String] + let addresses: [DBPUIUserProfileAddress] + let date: Double } /// Data representing the initial scan progress diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift index fb96638206..e1a17f590b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift @@ -32,13 +32,46 @@ extension Int { struct MirrorSite: Codable, Sendable { let name: String + let url: String let addedAt: Date let removedAt: Date? + + enum CodingKeys: CodingKey { + case name + case url + case addedAt + case removedAt + } + + init(name: String, url: String, addedAt: Date, removedAt: Date? = nil) { + self.name = name + self.url = url + self.addedAt = addedAt + self.removedAt = removedAt + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + + // The older versions of the JSON file did not have a URL property. + // When decoding those cases, we fallback to its name, since the name was the URL. + do { + url = try container.decode(String.self, forKey: .url) + } catch { + url = name + } + + addedAt = try container.decode(Date.self, forKey: .addedAt) + removedAt = try? container.decode(Date.self, forKey: .removedAt) + + } } struct DataBroker: Codable, Sendable { let id: Int64? let name: String + let url: String let steps: [Step] let version: String let schedulingConfig: DataBrokerScheduleConfig @@ -51,6 +84,7 @@ struct DataBroker: Codable, Sendable { enum CodingKeys: CodingKey { case name + case url case steps case version case schedulingConfig @@ -60,6 +94,7 @@ struct DataBroker: Codable, Sendable { init(id: Int64? = nil, name: String, + url: String, steps: [Step], version: String, schedulingConfig: DataBrokerScheduleConfig, @@ -68,6 +103,13 @@ struct DataBroker: Codable, Sendable { ) { self.id = id self.name = name + + if url.isEmpty { + self.url = name + } else { + self.url = url + } + self.steps = steps self.version = version self.schedulingConfig = schedulingConfig @@ -78,6 +120,15 @@ struct DataBroker: Codable, Sendable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) + + // The older versions of the JSON file did not have a URL property. + // When decoding those cases, we fallback to its name, since the name was the URL. + do { + url = try container.decode(String.self, forKey: .url) + } catch { + url = name + } + version = try container.decode(String.self, forKey: .version) steps = try container.decode([Step].self, forKey: .steps) schedulingConfig = try container.decode(DataBrokerScheduleConfig.self, forKey: .schedulingConfig) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index 5777596838..37cd385e4e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -99,7 +99,7 @@ extension DataBrokerOperation { if action.needsEmail { do { stageCalculator?.setStage(.emailGenerate) - extractedProfile?.email = try await emailService.getEmail(dataBrokerName: query.dataBroker.name) + extractedProfile?.email = try await emailService.getEmail(dataBrokerURL: query.dataBroker.url) stageCalculator?.fireOptOutEmailGenerate() } catch { await onError(error: DataBrokerProtectionError.emailError(error as? EmailError)) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index 28c2128a00..f3203334e2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -152,7 +152,7 @@ public struct DataBrokerProtectionBrokerUpdater { // 2. If does exist, we check the number version, if the version number is new, we update it // 3. If it does not exist, we add it, and we create the scan operations related to it private func update(_ broker: DataBroker) throws { - guard let savedBroker = try vault.fetchBroker(with: broker.name) else { + guard let savedBroker = try vault.fetchBroker(with: broker.url) else { // The broker does not exist in the current storage. We need to add it. try add(broker) return diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json index 60823c78ac..ee35af6d17 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json @@ -1,8 +1,9 @@ { - "name": "advancedbackgroundchecks.com", - "version": "0.1.0", + "name": "AdvancedBackgroundChecks", + "url": "advancedbackgroundchecks.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678078800000, + "addedDatetime": 1678082400000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "ef8031e6-5e61-4183-b57e-7df156c7129a", + "id": "c73ba931-9e01-4d37-9e15-2fd7a14eefa3", "url": "https://www.advancedbackgroundchecks.com/names/${firstName}-${lastName}_${city}-${state}_age_${age}" }, { "actionType": "extract", - "id": "f3ed744c-6cfc-4a99-b46e-6095587eadfc", + "id": "94003082-0a9d-4418-ac88-68595c7f4953", "selector": ".card-block", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json index 5b3800cfcf..552923ca1f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json @@ -1,6 +1,7 @@ { "name": "backgroundcheck.run", - "version": "0.1.1", + "url": "backgroundcheck.run", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1677736800000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "aa12b430-8e5d-4c64-bb77-2961f19a1bc8", + "id": "5f90e39f-cb94-4b8d-94ed-48ba0060dc08", "url": "https://backgroundcheck.run/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}" }, { "actionType": "extract", - "id": "75fd2e16-d84a-4bbe-9cf1-79c6d1cc4dec", + "id": "3225fa15-4e00-4e6a-bfc7-a85dfb504c86", "selector": ".b-pfl-list", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json index 130c996369..bb15f0093f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json @@ -1,8 +1,9 @@ { - "name": "centeda.com", - "version": "0.1.1", + "name": "Centeda", + "url": "centeda.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "25990359-3d58-45de-bdfd-d524b1946e57", + "id": "2f6639c0-201f-4d5e-8467-ae0ba457b409", "url": "https://centeda.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "7108af78-dbbf-47ec-8bb9-e44be505993e", + "id": "e2e236b0-515b-43b3-9154-0432ed9b7566", "selector": ".search-item", "profile": { "name": { @@ -63,4 +64,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json index f871133c15..8b6801fc48 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json @@ -1,6 +1,7 @@ { - "name": "clubset.com", - "version": "0.1.1", + "name": "Clubset", + "url": "clubset.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "917f5d40-2011-4fe5-9ef6-136d6bfaea35", + "id": "5c559c67-c13c-4055-a318-6ba35d62a2cf", "url": "https://clubset.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state|upcase}&city=${city|capitalize}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "06e37215-ef34-4971-bf86-e5a03dfe46e8", + "id": "866bdfc5-069e-4734-9ce0-a19976fa796b", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json index 0aca895c02..4c2bd20999 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json @@ -1,8 +1,9 @@ { - "name": "clustrmaps.com", - "version": "0.1.1", + "name": "ClustrMaps", + "url": "clustrmaps.com", + "version": "0.1.4", "parent": "neighbor.report", - "addedDatetime": 1692590400000, + "addedDatetime": 1692594000000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "a39655de-5c23-477d-9887-1d34966a1069", + "id": "e6929e37-4764-450a-be2a-73479f11842a", "url": "https://clustrmaps.com/persons/${firstName}-${lastName}/${state|stateFull|capitalize}/${city|hyphenated}" }, { "actionType": "extract", - "id": "4e3a628e-3634-4a2b-b632-4fbb8ce0b52b", + "id": "06f39df7-89c2-40da-b288-cdf3ed0e4bfd", "selector": ".//div[@itemprop='Person']", "profile": { "name": { @@ -55,4 +56,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json index d68f6b9f4c..3df8d7f195 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json @@ -1,6 +1,7 @@ { - "name": "councilon.com", - "version": "0.1.1", + "name": "Councilon", + "url": "councilon.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "295418e5-e1da-43b4-af50-75576ca4f843", + "id": "a5052dda-d4e7-4d3f-97bc-ef9f0aa9ae5f", "url": "https://councilon.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "eead1b72-7d6b-4cdc-988d-5ea66eb398f1", + "id": "55a50a37-9b1b-40fa-8533-af1273a26258", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json index 33f27d0c79..3b59ed585e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json @@ -1,8 +1,9 @@ { - "name": "curadvisor.com", - "version": "0.1.1", + "name": "CurAdvisor", + "url": "curadvisor.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677736800000, + "addedDatetime": 1703052000000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "bdb69f52-8ece-4d65-9b78-543fef0e90ae", + "id": "ab5503c7-bd11-4320-b38e-c637b239182e", "url": "https://curadvisor.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "3b9bc992-ecc0-4dc5-b716-fcea021cbcdb", + "id": "d273c1cf-2635-40d7-b26f-6f34467282cf", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json index e27b744790..f930834db9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json @@ -1,6 +1,7 @@ { - "name": "cyberbackgroundchecks.com", - "version": "0.1.1", + "name": "Cyber Background Checks", + "url": "cyberbackgroundchecks.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1705644000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "4b7037f3-9c6a-42b5-929e-621256e0a044", + "id": "d8c84470-d8b3-4c46-a645-01cc6b139b3b", "url": "https://www.cyberbackgroundchecks.com/people/${firstName}-${lastName}/${state}/${city}" }, { "actionType": "extract", - "id": "f36a73d7-9efb-452e-8c60-6d9df2964bcf", + "id": "b4c12cf2-0fd6-4209-8816-3bf2cce23cde", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json index 3dfe8e5431..9949e04675 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json @@ -1,8 +1,9 @@ { - "name": "dataveria.com", - "version": "0.1.1", + "name": "Dataveria", + "url": "dataveria.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "fc449310-7b7b-45d4-bcf9-0c5d51c246f8", + "id": "a8f3a259-2d39-4ae3-ac13-65aa63a53331", "url": "https://dataveria.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "0481dc49-43e8-4af0-b697-680fb57ec24b", + "id": "e810cc23-2d2a-4e6e-b06f-dfc8a2e1e85d", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json index 4462e0c86d..9c1129a333 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json @@ -1,8 +1,9 @@ { - "name": "fastbackgroundcheck.com", - "version": "0.1.1", + "name": "FastBackgroundCheck.com", + "url": "fastbackgroundcheck.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678082400000, + "addedDatetime": 1706248800000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "2a3a5979-9de0-44b2-ae03-f25422f0c2aa", + "id": "997adf8d-023c-409e-9206-57871cd25f0a", "url": "https://www.fastbackgroundcheck.com/people/${firstName}-${lastName}/${city}-${state}" }, { "actionType": "extract", - "id": "4818ff1c-d419-44c2-8168-501b456c6c6a", + "id": "2f531e34-2ac0-4743-a760-065187d6c951", "selector": ".person-container", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json index 2c215abdf6..c448989448 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json @@ -1,6 +1,7 @@ { - "name": "freepeopledirectory.com", - "version": "0.1.1", + "name": "FreePeopleDirectory", + "url": "freepeopledirectory.com", + "version": "0.1.4", "parent": "spokeo.com", "addedDatetime": 1674540000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "815a1cd3-2577-4f43-a163-0cf4d22e66a4", + "id": "4c607417-36bc-47d4-8562-9c2244db354d", "url": "https://www.freepeopledirectory.com/name/${firstName}-${lastName}/${state|upcase}/${city}" }, { "actionType": "extract", - "id": "10738ba0-bc6b-42ba-a37c-487ff3927dd5", + "id": "a1637310-ca7a-40b0-b2f5-db22b43b5d54", "selector": ".whole-card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json index 961bb83ae3..2c035a980c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json @@ -1,8 +1,9 @@ { - "name": "inforver.com", - "version": "0.1.1", + "name": "Inforver", + "url": "inforver.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "a56ab792-fc1b-4e60-b0b9-0bd4f580476f", + "id": "85fac850-36ad-4d9c-ad7c-c1250c7b5585", "url": "https://inforver.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "591ba784-106c-421b-b188-a376f1f9cb01", + "id": "e5e9c1b0-4af4-4fb6-bd2d-7d026ffd95e7", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json index 31b5dc20a8..5f7e750909 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json @@ -1,6 +1,7 @@ { - "name": "kwold.com", - "version": "0.1.1", + "name": "Kwold", + "url": "kwold.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "47152fc1-79d5-4bcc-b930-6b5cdc66e972", + "id": "936eee30-d31e-48fb-8cc4-9391869934b9", "url": "https://kwold.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "50507ab4-2e75-4f1d-af23-9725b9955bc3", + "id": "870ee174-275a-4ea8-b2d7-a222418e5de9", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json index d640212852..92a0d2af57 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json @@ -1,7 +1,8 @@ { - "name": "neighbor.report", + "name": "Neighbor Report", + "url": "neighbor.report", "version": "0.1.4", - "addedDatetime": 1703559600000, + "addedDatetime": 1703570400000, "steps": [ { "stepType": "scan", @@ -9,12 +10,12 @@ "actions": [ { "actionType": "navigate", - "id": "a554d7d2-f348-487a-97de-8d4f0d1d35c0", + "id": "bbaf8a18-fef8-42a6-9682-747b8ff485b2", "url": "https://neighbor.report/${firstName}-${lastName}/${state|stateFull|hyphenated}/${city|hyphenated}" }, { "actionType": "extract", - "id": "17f80250-1e3c-4e55-8e50-68fe98a6ce23", + "id": "0dac4a6d-1291-47c3-97b8-56200f751ac8", "selector": ".lstd", "profile": { "name": { @@ -50,12 +51,12 @@ "actions": [ { "actionType": "navigate", - "id": "59cc488d-e317-4fb4-8aaa-a20cb71f7480", + "id": "b1f7f4ab-51b0-4885-ba73-97be0822d0ba", "url": "https://neighbor.report/remove" }, { "actionType": "fillForm", - "id": "3a4c1775-941a-4f48-873d-c780f5ea25a0", + "id": "743afa6c-7dea-4115-934b-bea369307acd", "selector": ".form-horizontal", "elements": [ { @@ -74,17 +75,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "f470e245-d5ee-4908-bd6c-16e604a1a29b", + "id": "24ce0da0-7cc3-47e7-bf8e-6f5fe98b7a91", "selector": ".recaptcha-div" }, { "actionType": "solveCaptcha", - "id": "7f0a8fc6-32a3-4f4a-b61d-267f9666de91", + "id": "b720de9a-f519-466f-980d-d9c52d8870a2", "selector": ".recaptcha-div" }, { "actionType": "click", - "id": "7fbc5a97-bc57-41bc-a556-0fdfd8a0845d", + "id": "46690938-f112-4091-bd07-b5641e38151f", "elements": [ { "type": "button", @@ -94,7 +95,7 @@ }, { "actionType": "click", - "id": "d1513f65-a746-4597-9ed2-4cd5e40dead3", + "id": "07cfed17-9d75-471a-b6a0-0522add35ffa", "elements": [ { "type": "button", @@ -108,7 +109,7 @@ }, { "actionType": "expectation", - "id": "8acd9c96-443d-4593-a3a7-9efc9fd5070a", + "id": "ebd61347-60e1-4c19-bc41-dd1ce36d3138", "expectations": [ { "type": "text", @@ -125,4 +126,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json index ddb83134f9..54f8d23ac8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json @@ -1,6 +1,7 @@ { - "name": "newenglandfacts.com", - "version": "0.1.1", + "name": "New England Facts", + "url": "newenglandfacts.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "8bd7953c-ee22-49be-8937-a1798046a0c1", + "id": "05725a5a-ec3f-49c8-875b-ab9787b9385f", "url": "https://newenglandfacts.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "4012d312-2f7f-4cc1-bf7a-b7655f550c1a", + "id": "7f41b78a-bb65-4bb2-a6ca-1a6ab55890ce", "selector": ".b-pfl-list", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json index a7b4efd714..9cb63483be 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json @@ -1,8 +1,9 @@ { - "name": "officialusa.com", - "version": "0.1.0", + "name": "OfficialUSA", + "url": "officialusa.com", + "version": "0.1.4", "parent": "neighbor.report", - "addedDatetime": 1692590400000, + "addedDatetime": 1692594000000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "dad25b4c-743b-4bca-a395-05f1e76ef5c9", + "id": "b430e29e-89f0-4994-96b2-08d0cbdc388c", "url": "https://officialusa.com/names/${firstName}-${lastName}/" }, { "actionType": "extract", - "id": "b867d570-6124-40d9-9076-7ee0fa5b4d68", + "id": "d989f3b7-9b8a-44a6-a51e-70762255f3fc", "selector": ".person", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json index 7b1d26eb38..b8550c93e4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json @@ -1,6 +1,7 @@ { - "name": "people-background-check.com", - "version": "0.1.1", + "name": "People Background Check", + "url": "people-background-check.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "18e35c3b-b837-40e9-b353-20230d36bc4d", + "id": "6fee90c5-5f7e-4fd0-badf-069e2b94a65d", "url": "https://people-background-check.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}" }, { "actionType": "extract", - "id": "7a23b927-acfc-4d29-b4b6-3f204687619c", + "id": "ee03ba42-e9a5-4489-a7d6-d50bf21238aa", "selector": ".b-pfl-list", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json index 7e690167c8..34bc5b8770 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json @@ -1,7 +1,8 @@ { - "name": "peoplefinders.com", - "version": "0.1.0", - "addedDatetime": 1677128400000, + "name": "PeopleFinders", + "url": "peoplefinders.com", + "version": "0.1.4", + "addedDatetime": 1677132000000, "steps": [ { "stepType": "scan", @@ -9,12 +10,12 @@ "actions": [ { "actionType": "navigate", - "id": "aafba5bd-a157-4e35-b653-0797a732d94c", + "id": "71c7cb2f-14fe-43b8-9623-452b8bd10d4e", "url": "https://www.peoplefinders.com/people/${firstName}-${lastName}/${state}/${city}?landing=all&age=${age}" }, { "actionType": "extract", - "id": "b8f10f20-3363-4781-a03b-c4958b6269c7", + "id": "5c5af912-091f-4f48-922f-ba554951ddd9", "selector": ".record", "profile": { "name": { @@ -48,12 +49,12 @@ "actions": [ { "actionType": "navigate", - "id": "f5fbd4f5-23f7-45ed-a9ce-3e9b0a5a7a0a", + "id": "4b065fde-35c7-43d7-aed6-3abcdac94f08", "url": "https://www.peoplefinders.com/opt-out" }, { "actionType": "click", - "id": "7b33cd1b-3948-4454-8434-e703cc235123", + "id": "b5c0929e-e362-4570-815b-0433ef97fddf", "elements": [ { "type": "button", @@ -63,7 +64,7 @@ }, { "actionType": "fillForm", - "id": "32056b7a-dc80-4d5d-b9cc-dccd32cb56be", + "id": "2fb91804-e5ea-414e-9354-fba98f3c00e1", "selector": ".opt-out-form", "elements": [ { @@ -78,17 +79,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "5d5068aa-5c16-4fdc-8f3b-e412ad4eabed", + "id": "4b9706ef-dd9b-47d6-b337-12f66a5f9138", "selector": ".g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "3443e060-8aee-4bd0-ab2c-ea03f8b8f93c", + "id": "770019d3-fa88-400a-8480-7cc31d6b3382", "selector": ".g-recaptcha" }, { "actionType": "click", - "id": "cb9ef5b0-0155-42f1-a766-145b3c14586b", + "id": "a7285f44-6c99-44b1-8199-eb6c383fe12b", "elements": [ { "type": "button", @@ -98,22 +99,22 @@ }, { "actionType": "emailConfirmation", - "id": "5cc7cfa5-e8ab-4dc1-b58b-973af3d3f364", + "id": "05cc08ea-fb80-40fb-8cce-3ca674eea03b", "pollingTime": 30 }, { "actionType": "getCaptchaInfo", - "id": "3a44c15d-1dd0-4e92-beba-bf3d8544c6e9", + "id": "8cb4256a-b162-407f-8434-5536c7560c98", "selector": ".g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "d2566371-8b02-4414-9a24-1f9d2761eb1d", + "id": "38c64eec-6bd9-4751-a7cf-8cbe9901b0f6", "selector": ".g-recaptcha" }, { "actionType": "click", - "id": "259d8895-ac58-46b0-a209-7f209171e13c", + "id": "d1d25423-912b-4828-825b-eb83809ada08", "elements": [ { "type": "button", @@ -123,7 +124,7 @@ }, { "actionType": "expectation", - "id": "ed02f55b-67b3-4efc-a3cc-ce6b6c7ceeed", + "id": "fdb755da-8970-426f-b09e-12165c2169dd", "expectations": [ { "type": "url", @@ -139,4 +140,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json index 6e477c6e13..6189f3d311 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json @@ -1,6 +1,7 @@ { - "name": "peoplesearchnow.com", - "version": "0.1.1", + "name": "People Search Now", + "url": "peoplesearchnow.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1705989600000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "b6994b26-9904-407b-9bcf-0fd6f809771d", + "id": "db9e093d-68c2-45e1-a529-29a2dc67dfab", "url": "https://peoplesearchnow.com/person/${firstName}-${lastName}_${city}_${state}/" }, { "actionType": "extract", - "id": "4e7f0e9a-1d24-47c0-886f-a08d88074878", + "id": "78912133-761b-4971-9780-4e16c8dd43b2", "selector": ".result-search-block", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json index 503392f378..815fdcb8fc 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json @@ -1,8 +1,9 @@ { - "name": "pub360.com", - "version": "0.1.1", + "name": "Pub360", + "url": "pub360.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "72fc91c4-e8bc-4656-8260-cd3bb15e2001", + "id": "8e2a1251-2685-476a-b4c1-53d138331abe", "url": "https://pub360.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "2cb4778d-e3d6-4432-8421-84438c280e19", + "id": "9ce62e6f-b103-45f6-9f92-56785eb22320", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json index 991d4d2b2c..5ea3d241e4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json @@ -1,6 +1,7 @@ { - "name": "publicreports.com", - "version": "0.1.1", + "name": "PublicReports", + "url": "publicreports.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "ae0104a2-a75c-4d97-bada-dda4f21dd446", + "id": "b995b1bf-6610-4085-9d07-d38857807535", "url": "https://publicreports.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "388c55e3-fa12-4376-9a02-01190b8a30fd", + "id": "7fb121fb-e2a0-4fa2-9b97-51130104971c", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json index 7409fd240b..e8b18f9ec8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json @@ -1,6 +1,7 @@ { - "name": "quickpeopletrace.com", - "version": "0.1.1", + "name": "Quick People Trace", + "url": "quickpeopletrace.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1674540000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "45443ab2-7563-4c7d-8bf2-b1b550f4b825", + "id": "2db8c120-a8c3-4aa0-a9ce-b075ca85fc68", "url": "https://www.quickpeopletrace.com/search/?addresssearch=1&tabid=1&teaser-firstname=${firstName}&teaser-middlename=&teaser-lastname=${lastName}&teaser-city=${city}&teaser-state=${state|upcase}&teaser-submitted=Search" }, { "actionType": "extract", - "id": "08607047-96e8-4fbb-9af9-bf7b8e163b20", + "id": "bd48b737-89c4-408a-a28c-2dfa828aebd8", "selector": "//table/tbody/tr[position() > 1]", "profile": { "name": { @@ -24,9 +25,6 @@ "age": { "selector": ".//td[3]" }, - "addressCityState": { - "selector": ".//td[4]/strong" - }, "addressCityStateList": { "selector": ".//td[4]" }, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json index 535ec0f63d..4a68c912e3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json @@ -1,6 +1,7 @@ { - "name": "searchpeoplefree.com", - "version": "0.1.1", + "name": "Search People FREE", + "url": "searchpeoplefree.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "2b537ff2-8967-465c-ad5c-c4c2d31f60e1", + "id": "f5bad072-6f55-4357-b23b-1df4c9584e67", "url": "https://searchpeoplefree.com/find/${firstName}-${lastName}/${state}/${city}" }, { "actionType": "extract", - "id": "70728718-fe02-43f6-b86f-6d6c6bbbf009", + "id": "749fb8fe-9994-41e2-a0ea-ae6334c5aee0", "selector": "//li[@class='toc l-i mb-5']", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json index 31424db72d..23f588c796 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json @@ -1,6 +1,7 @@ { - "name": "smartbackgroundchecks.com", - "version": "0.1.1", + "name": "SmartBackgroundChecks", + "url": "smartbackgroundchecks.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1678082400000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "97b307c8-e3e4-4090-a6ab-c5eeb599d248", + "id": "1c6bdc6e-12dd-47db-b5b0-13055c1f3d5d", "url": "https://www.smartbackgroundchecks.com/people/${firstName}-${lastName}/${city}/${state}" }, { "actionType": "extract", - "id": "ca20a933-b703-427e-8cbf-e2f25cd763a6", + "id": "ac554b4f-e4a0-44c5-81a6-c04e46e4ce3b", "selector": ".card-block", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json index 2618099829..3c0f0008f8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json @@ -1,12 +1,33 @@ { - "name": "spokeo.com", - "version": "0.1.3", + "name": "Spokeo", + "url": "spokeo.com", + "version": "0.1.4", "addedDatetime": 1692594000000, "mirrorSites": [ - { "name": "callersmart.com", "addedAt": 1705599286529, "removedAt": null }, - { "name": "selfie.network", "addedAt": 1705599286529, "removedAt": null }, - { "name": "selfie.systems", "addedAt": 1705599286529, "removedAt": null }, - { "name": "peoplewin.com", "addedAt": 1705599286529, "removedAt": null } + { + "name": "CallerSmart", + "url": "callersmart.com", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "Selfie Network", + "url": "selfie.network", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "Selfie Systems", + "url": "selfie.systems", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "PeopleWin", + "url": "peoplewin.com", + "addedAt": 1705599286529, + "removedAt": null + } ], "steps": [ { @@ -15,12 +36,12 @@ "actions": [ { "actionType": "navigate", - "id": "d3174bd8-3253-45e3-88f0-1366882a2df7", + "id": "9b617d27-b330-46fc-bdb0-6239c0873897", "url": "https://www.spokeo.com/${firstName}-${lastName}/${state|stateFull}/${city}" }, { "actionType": "extract", - "id": "e47f5f27-dfbf-4f2c-8d7a-43f581abdaa2", + "id": "4f7124c2-bd8c-4649-84f2-04f0962225b5", "selector": ".single-column-list-item", "profile": { "name": { @@ -52,12 +73,12 @@ "actions": [ { "actionType": "navigate", - "id": "dba8f444-a433-4ad2-9819-3c555bfedd9c", + "id": "df75e4fb-f14b-4b65-afe2-82e03b71c6a9", "url": "https://www.spokeo.com/optout" }, { "actionType": "fillForm", - "id": "b1145fca-3e35-4ee9-86f2-e35c393846d3", + "id": "42cbfc2b-d96b-4bd6-8d16-0542a672d869", "selector": ".optout_container", "elements": [ { @@ -72,17 +93,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "8f9608b4-bbf7-4540-8b22-5c381225cd02", + "id": "e1581b9e-7460-4bbd-a010-634c2db12ca1", "selector": "#g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "a5a884b8-12f6-4029-aa80-244f1a163f67", + "id": "01ca39d9-e842-41cf-b0f9-a7d517bc0dd6", "selector": "#g-recaptcha" }, { "actionType": "click", - "id": "a7d4fdd4-30b8-46f7-8700-67c633da1f91", + "id": "7556edd5-570b-4c4a-acc7-f1066138d513", "elements": [ { "type": "button", @@ -92,7 +113,7 @@ }, { "actionType": "expectation", - "id": "d4a804a3-de62-4f66-a56e-e9d7e65fb8bb", + "id": "f7b5125e-0dda-4a14-8943-8c20c09125bc", "expectations": [ { "type": "text", @@ -103,12 +124,12 @@ }, { "actionType": "emailConfirmation", - "id": "5138062c-99d3-4523-b222-8123b13bc524", + "id": "dbd875b6-bdc7-48ca-962b-885941e6284a", "pollingTime": 30 }, { "actionType": "expectation", - "id": "cc14f3ea-35f8-4d31-a280-dc97526de12a", + "id": "b2f1c371-d779-4b3b-8516-0d13169cf873", "expectations": [ { "type": "text", @@ -125,4 +146,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json index 84d468f943..a226c959a0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json @@ -1,6 +1,7 @@ { - "name": "truepeoplesearch.com", - "version": "0.1.1", + "name": "TruePeopleSearch", + "url": "truepeoplesearch.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1703138400000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "4b7d8751-d3cd-4e4b-b2a2-66219eb6a8e8", + "id": "12eb70c1-53d5-4881-9dce-74ed4fada583", "url": "https://www.truepeoplesearch.com/results?name=${firstName}%20${lastName}&citystatezip=${city|capitalize},${state|upcase}" }, { "actionType": "extract", - "id": "cdb5940a-8505-4b28-9699-d98235e1fff1", + "id": "881e0e21-c375-4083-a9be-86f82063849b", "selector": ".card-summary", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json index 71c8e711ed..b8fba84277 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json @@ -1,8 +1,9 @@ { - "name": "usa-people-search.com", - "version": "0.1.1", + "name": "USA People Search", + "url": "usa-people-search.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678078800000, + "addedDatetime": 1678082400000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "2c4b31a3-661b-4f30-a4d3-b5a4a13c95db", + "id": "67e80e69-f542-4714-8705-c43af630ac72", "url": "https://usa-people-search.com/name/${firstName|downcase}-${lastName|downcase}/${city|downcase}-${state|stateFull|downcase}?age=${age}" }, { "actionType": "extract", - "id": "20a4d510-56b6-46a8-92ce-be16ed3ce049", + "id": "c0a82b15-7564-4e12-8c4e-084174242623", "selector": ".card-block", "profile": { "name": { @@ -62,4 +63,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json index 412a14d7d9..4645c85dd0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json @@ -1,6 +1,7 @@ { - "name": "usatrace.com", - "version": "0.1.1", + "name": "USA Trace", + "url": "usatrace.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1674540000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "5071e480-88ae-49b5-91b6-1daf26c55acf", + "id": "17217b04-28ae-4262-aa33-ee3695bb6bd6", "url": "https://www.usatrace.com/people-search/${firstName}-${lastName}/${city}-${state|upcase}" }, { "actionType": "extract", - "id": "3237fc09-247c-4942-9920-9bbb937f6ac2", + "id": "426d8e8a-2f32-46f3-9d1d-e7f6e2fddadb", "selector": "//table/tbody/tr[position() > 1]", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json index 0770b2f474..5aff073d4a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json @@ -1,8 +1,9 @@ { - "name": "usphonebook.com", - "version": "0.1.1", + "name": "USPhoneBook", + "url": "usphonebook.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678078800000, + "addedDatetime": 1678082400000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "f214150b-4f02-46e1-b7ea-81f6bb1bf097", + "id": "6ee93554-95da-4a36-a7f7-c059d8f53ca3", "url": "https://www.usphonebook.com/${firstName}-${lastName}/${state|stateFull}/${city}" }, { "actionType": "extract", - "id": "af98bb63-b885-4f47-bb47-5f9ec5b491a4", + "id": "fffae12f-4ca1-4a8f-81b9-00adf0487129", "selector": ".ls_contacts-people-finder-wrapper", "profile": { "name": { @@ -56,4 +57,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json index f493fbd347..5aff5bd46e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json @@ -1,7 +1,8 @@ { - "name": "verecor.com", - "version": "0.1.2", - "addedDatetime": 1677128400000, + "name": "Verecor", + "url": "verecor.com", + "version": "0.1.4", + "addedDatetime": 1677132000000, "steps": [ { "stepType": "scan", @@ -9,7 +10,7 @@ "actions": [ { "actionType": "navigate", - "id": "6f53d146-af6a-4bce-970d-f1dcbc496037", + "id": "37fc63a6-e434-4ba0-9e9e-d80898e4dfa4", "url": "https://verecor.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -23,7 +24,7 @@ }, { "actionType": "extract", - "id": "e8c09200-030c-492a-8e54-22bc6bdb6829", + "id": "a955924c-7959-48c8-9511-3f843baed729", "selector": ".search-item", "profile": { "name": { @@ -58,12 +59,12 @@ "actions": [ { "actionType": "navigate", - "id": "f4bbe480-a6ff-40a5-aa25-8ff9ac40c9bf", + "id": "85cd9682-94d8-46ac-9999-e03dfa9f8d4e", "url": "https://verecor.com/ng/control/privacy" }, { "actionType": "fillForm", - "id": "1e6302e1-daf9-49d6-951f-506ea5e266a0", + "id": "ed45c76b-e537-4072-9f46-9515c6e215be", "selector": ".ahm", "elements": [ { @@ -82,17 +83,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "6a3dc470-3bf7-4b8b-bb44-f77ef1a2c540", + "id": "0e1474f0-24fe-4f6a-8d2e-2dfd91cf574b", "selector": ".g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "83157244-c5bf-44a9-979c-679e1404d67d", + "id": "52a858f5-7dc5-40aa-aaa7-7090e06ea55e", "selector": ".g-recaptcha" }, { "actionType": "click", - "id": "15c50d7f-0e72-4509-be2b-40cde34b48e6", + "id": "759e0dd2-3a93-42a8-9a83-5e3408f5566b", "elements": [ { "type": "button", @@ -102,7 +103,7 @@ }, { "actionType": "expectation", - "id": "2ed336a2-a7a9-4cbd-933c-cd463df4f553", + "id": "089924be-5ea3-48a9-a325-8976d262f39b", "expectations": [ { "type": "text", @@ -113,12 +114,12 @@ }, { "actionType": "emailConfirmation", - "id": "88c09081-e848-4e75-a7b9-3ee28e95a459", + "id": "8094718e-412a-418f-b74d-cd4fc5e42c56", "pollingTime": 30 }, { "actionType": "expectation", - "id": "dd03cf9f-8227-4881-86bf-09ce158bf151", + "id": "af8fb89b-88d2-4901-b90c-eaac3c7566db", "expectations": [ { "type": "text", @@ -135,4 +136,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json index 814e908fae..c7a3bad5c2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json @@ -1,8 +1,9 @@ { - "name": "vericora.com", - "version": "0.1.1", + "name": "Vericora", + "url": "vericora.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "9488e141-d109-4cbf-bc65-1b9036728ff4", + "id": "69175f1a-0024-4efd-ab3e-67bcf915a770", "url": "https://vericora.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "baaecb74-8d63-496c-a3e0-a8acbdee2c99", + "id": "bd941009-4462-4d59-ba44-46250f580531", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json index 121bc68d3e..5f4f307f92 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json @@ -1,8 +1,9 @@ { - "name": "veriforia.com", - "version": "0.1.1", + "name": "Veriforia", + "url": "veriforia.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "ffb30e97-b03f-4157-a511-09ad8ffb8b54", + "id": "17442975-944c-4b01-8518-7f1dff171ad2", "url": "https://veriforia.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "1d8d9c20-9897-4386-8bc1-bd591abe7c81", + "id": "32e963e1-4959-4e5e-981b-550f1bf36f9a", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json index 43d95caf8a..61becad701 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json @@ -1,8 +1,9 @@ { - "name": "veripages.com", - "version": "0.1.2", + "name": "Veripages", + "url": "veripages.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1691982000000, + "addedDatetime": 1691989200000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "5bf98772-1804-4939-a06b-dbf9cd31f198", + "id": "2346b569-1c46-4ef9-8ea0-fa18bea967fa", "url": "https://veripages.com/inner/profile/search?fname=${firstName}&lname=${lastName}&fage=${age|ageRange}&state=${state}&city=${city}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "f3d53642-9a58-4275-b97f-5547e3ef8e55", + "id": "c4281ca8-d4d0-4091-b6c2-3094801e99c0", "selector": ".search-item", "profile": { "name": { @@ -66,4 +67,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json index d12865681f..3d94019338 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json @@ -1,6 +1,7 @@ { - "name": "virtory.com", - "version": "0.1.1", + "name": "Virtory", + "url": "virtory.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "99465bd6-ce87-4fc6-96a2-eea8137e4a30", + "id": "0568e4f5-73c2-4b1a-9eb6-ac3571b1a01e", "url": "https://virtory.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "8d66ac98-f788-4fbd-acec-56034682b4b1", + "id": "df2216f3-0890-4d13-b2aa-233084167720", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json index b4fd3669ec..12c43b7fa4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json @@ -1,6 +1,7 @@ { - "name": "wellnut.com", - "version": "0.1.1", + "name": "Wellnut", + "url": "wellnut.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "b9db3c1e-ece6-45d1-94ec-1143da9607aa", + "id": "a38752f3-ae69-45c3-ba3f-3a73e549e644", "url": "https://wellnut.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "e4b7c983-c96e-4ce8-8703-3cb319454db7", + "id": "b7747e92-5fe5-46f7-b083-5df6fbdc2b84", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index 0e6b930d71..c61178c0f9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -106,7 +106,6 @@ final class DataBrokerProtectionProcessor { completion: @escaping () -> Void) { // Before running new operations we check if there is any updates to the broker files. - // This runs only once per 24 hours. if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) { let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault) brokerUpdater.checkForUpdatesInBrokerJSONFiles() diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift index 75b80b195a..038c72c2d8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift @@ -30,7 +30,7 @@ public enum EmailError: Error, Equatable, Codable { } protocol EmailServiceProtocol { - func getEmail(dataBrokerName: String?) async throws -> String + func getEmail(dataBrokerURL: String?) async throws -> String func getConfirmationLink(from email: String, numberOfRetries: Int, pollingIntervalInSeconds: Int, @@ -51,10 +51,10 @@ struct EmailService: EmailServiceProtocol { self.redeemUseCase = redeemUseCase } - func getEmail(dataBrokerName: String? = nil) async throws -> String { + func getEmail(dataBrokerURL: String? = nil) async throws -> String { var urlString = Constants.baseUrl + "/generate" - if let dataBrokerValue = dataBrokerName { + if let dataBrokerValue = dataBrokerURL { urlString += "?dataBroker=\(dataBrokerValue)" } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift index ab59f166cc..a70ad31818 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift @@ -34,7 +34,7 @@ protocol DataBrokerProtectionDatabaseProvider: SecureStorageDatabaseProvider { func save(_ broker: BrokerDB) throws -> Int64 func update(_ broker: BrokerDB) throws func fetchBroker(with id: Int64) throws -> BrokerDB? - func fetchBroker(with name: String) throws -> BrokerDB? + func fetchBroker(with url: String) throws -> BrokerDB? func fetchAllBrokers() throws -> [BrokerDB] func save(_ profileQuery: ProfileQueryDB) throws -> Int64 @@ -85,6 +85,8 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba public init(file: URL = DefaultDataBrokerProtectionDatabaseProvider.defaultDatabaseURL(), key: Data) throws { try super.init(file: file, key: key, writerType: .pool) { migrator in migrator.registerMigration("v1", migrate: Self.migrateV1(database:)) + migrator.registerMigration("v2", migrate: Self.migrateV2(database:)) + } } @@ -259,6 +261,16 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba $0.column(OptOutAttemptDB.Columns.startDate.name, .date).notNull() } } + + static func migrateV2(database: Database) throws { + try database.alter(table: BrokerDB.databaseTableName) { + $0.add(column: BrokerDB.Columns.url.name, .text) + } + try database.execute(sql: """ + UPDATE \(BrokerDB.databaseTableName) SET \(BrokerDB.Columns.url.name) = \(BrokerDB.Columns.name.name) + """) + } + // swiftlint:enable function_body_length func updateProfile(profile: DataBrokerProtectionProfile, mapperToDB: MapperToDB) throws -> Int64 { @@ -359,10 +371,10 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba } } - func fetchBroker(with name: String) throws -> BrokerDB? { + func fetchBroker(with url: String) throws -> BrokerDB? { try db.read { db in return try BrokerDB - .filter(Column(BrokerDB.Columns.name.name) == name) + .filter(Column(BrokerDB.Columns.url.name) == url) .fetchOne(db) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift index 267b013ed8..56b08dd5f2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift @@ -58,7 +58,7 @@ struct MapperToDB { func mapToDB(_ broker: DataBroker, id: Int64? = nil) throws -> BrokerDB { let encodedBroker = try jsonEncoder.encode(broker) - return .init(id: id, name: broker.name, json: encodedBroker, version: broker.version) + return .init(id: id, name: broker.name, json: encodedBroker, version: broker.version, url: broker.url) } func mapToDB(_ profileQuery: ProfileQuery, relatedTo profileId: Int64) throws -> ProfileQueryDB { @@ -171,6 +171,7 @@ struct MapperToModel { return DataBroker( id: brokerDB.id, name: decodedBroker.name, + url: decodedBroker.url, steps: decodedBroker.steps, version: decodedBroker.version, schedulingConfig: decodedBroker.schedulingConfig, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift index 348d99180d..a772e4d543 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift @@ -94,6 +94,7 @@ struct BrokerDB: Codable { let name: String let json: Data let version: String + let url: String } extension BrokerDB: PersistableRecord, FetchableRecord { @@ -104,6 +105,7 @@ extension BrokerDB: PersistableRecord, FetchableRecord { case name case json case version + case url } init(row: Row) throws { @@ -111,6 +113,7 @@ extension BrokerDB: PersistableRecord, FetchableRecord { name = row[Columns.name] json = row[Columns.json] version = row[Columns.version] + url = row[Columns.url] } func encode(to container: inout PersistenceContainer) throws { @@ -118,6 +121,7 @@ extension BrokerDB: PersistableRecord, FetchableRecord { container[Columns.name] = name container[Columns.json] = json container[Columns.version] = version + container[Columns.url] = url } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift index 63b1b4a47e..9f77b8a675 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift @@ -36,6 +36,7 @@ protocol DBPUICommunicationDelegate: AnyObject { func startScanAndOptOut() -> Bool func getInitialScanState() async -> DBPUIInitialScanState func getMaintananceScanState() async -> DBPUIScanAndOptOutMaintenanceState + func getDataBrokers() async -> [DBPUIDataBroker] } enum DBPUIReceivedMethodName: String { @@ -53,6 +54,7 @@ enum DBPUIReceivedMethodName: String { case startScanAndOptOut case initialScanStatus case maintenanceScanStatus + case getDataBrokers } enum DBPUISendableMethodName: String { @@ -69,7 +71,7 @@ struct DBPUICommunicationLayer: Subfeature { weak var delegate: DBPUICommunicationDelegate? private enum Constants { - static let version = 1 + static let version = 2 } internal init(webURLSettings: DataBrokerProtectionWebUIURLSettingsRepresentable) { @@ -101,6 +103,7 @@ struct DBPUICommunicationLayer: Subfeature { case .startScanAndOptOut: return startScanAndOptOut case .initialScanStatus: return initialScanStatus case .maintenanceScanStatus: return maintenanceScanStatus + case .getDataBrokers: return getDataBrokers } } @@ -264,6 +267,11 @@ struct DBPUICommunicationLayer: Subfeature { return maintenanceScanStatus } + func getDataBrokers(params: Any, origin: WKScriptMessage) async throws -> Encodable? { + let dataBrokers = await delegate?.getDataBrokers() ?? [DBPUIDataBroker]() + return DBPUIDataBrokerList(dataBrokers: dataBrokers) + } + func sendMessageToUI(method: DBPUISendableMethodName, params: DBPUISendableMessage, into webView: WKWebView) { broker?.push(method: method.rawValue, params: params, for: self, into: webView) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index 836bd52a30..454f1c75a9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -26,22 +26,24 @@ struct MapperToUI { name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map(mapToUI) ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), - relatives: extractedProfile.relatives ?? [String]() + relatives: extractedProfile.relatives ?? [String](), + date: extractedProfile.removedDate?.timeIntervalSince1970 ) } - func mapToUI(_ dataBrokerName: String, extractedProfile: ExtractedProfile) -> DBPUIDataBrokerProfileMatch { + func mapToUI(_ dataBrokerName: String, databrokerURL: String, extractedProfile: ExtractedProfile) -> DBPUIDataBrokerProfileMatch { DBPUIDataBrokerProfileMatch( - dataBroker: DBPUIDataBroker(name: dataBrokerName), + dataBroker: DBPUIDataBroker(name: dataBrokerName, url: databrokerURL), name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map(mapToUI) ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), - relatives: extractedProfile.relatives ?? [String]() + relatives: extractedProfile.relatives ?? [String](), + date: extractedProfile.removedDate?.timeIntervalSince1970 ) } func mapToUI(_ dataBroker: DataBroker) -> DBPUIDataBroker { - DBPUIDataBroker(name: dataBroker.name) + DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url) } func mapToUI(_ address: AddressCityState) -> DBPUIUserProfileAddress { @@ -75,7 +77,7 @@ struct MapperToUI { if !$0.dataBroker.mirrorSites.isEmpty { let mirrorSitesMatches = $0.dataBroker.mirrorSites.compactMap { mirrorSite in if mirrorSite.shouldWeIncludeMirrorSite() { - return mapToUI(mirrorSite.name, extractedProfile: extractedProfile) + return mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) } return nil @@ -110,7 +112,7 @@ struct MapperToUI { if let closestMatchesFoundEvent = scanOperation.closestMatchesFoundEvent() { for mirrorSite in dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: closestMatchesFoundEvent.date) { - let mirrorSiteMatch = mapToUI(mirrorSite.name, extractedProfile: extractedProfile) + let mirrorSiteMatch = mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) if let extractedProfileRemovedDate = extractedProfile.removedDate, mirrorSite.shouldWeIncludeMirrorSite(for: extractedProfileRemovedDate) { @@ -124,11 +126,21 @@ struct MapperToUI { } let completedOptOutsDictionary = Dictionary(grouping: removedProfiles, by: { $0.dataBroker }) - let completedOptOuts = completedOptOutsDictionary.map { (key: DBPUIDataBroker, value: [DBPUIDataBrokerProfileMatch]) in - DBPUIOptOutMatch(dataBroker: key, matches: value.count) - } - let lastScans = getLastScanInformation(brokerProfileQueryData: brokerProfileQueryData) - let nextScans = getNextScansInformation(brokerProfileQueryData: brokerProfileQueryData) + let completedOptOuts: [DBPUIOptOutMatch] = completedOptOutsDictionary.compactMap { (key: DBPUIDataBroker, value: [DBPUIDataBrokerProfileMatch]) in + value.compactMap { match in + guard let removedDate = match.date else { return nil } + return DBPUIOptOutMatch(dataBroker: key, + matches: value.count, + name: match.name, + alternativeNames: match.alternativeNames, + addresses: match.addresses, + date: removedDate) + } + }.flatMap { $0 } + + let nearestScanByBrokerURL = nearestRunDates(for: brokerProfileQueryData) + let lastScans = getLastScanInformation(brokerProfileQueryData: brokerProfileQueryData, nearestScanOperationByBroker: nearestScanByBrokerURL) + let nextScans = getNextScansInformation(brokerProfileQueryData: brokerProfileQueryData, nearestScanOperationByBroker: nearestScanByBrokerURL) return DBPUIScanAndOptOutMaintenanceState( inProgressOptOuts: inProgressOptOuts, @@ -140,7 +152,8 @@ struct MapperToUI { private func getLastScanInformation(brokerProfileQueryData: [BrokerProfileQueryData], currentDate: Date = Date(), - format: String = "dd/MM/yyyy") -> DBUIScanDate { + format: String = "dd/MM/yyyy", + nearestScanOperationByBroker: [String: Date]) -> DBUIScanDate { let scansGroupedByLastRunDate = Dictionary(grouping: brokerProfileQueryData, by: { $0.scanOperationData.lastRunDate?.toFormat(format) }) let closestScansBeforeToday = scansGroupedByLastRunDate .filter { $0.key != nil && $0.key!.toDate(using: format) < currentDate } @@ -148,12 +161,13 @@ struct MapperToUI { .flatMap { [$0.key?.toDate(using: format): $0.value] } .last - return scanDate(element: closestScansBeforeToday) + return scanDate(element: closestScansBeforeToday, nearestScanOperationByBroker: nearestScanOperationByBroker) } private func getNextScansInformation(brokerProfileQueryData: [BrokerProfileQueryData], currentDate: Date = Date(), - format: String = "dd/MM/yyyy") -> DBUIScanDate { + format: String = "dd/MM/yyyy", + nearestScanOperationByBroker: [String: Date]) -> DBUIScanDate { let scansGroupedByPreferredRunDate = Dictionary(grouping: brokerProfileQueryData, by: { $0.scanOperationData.preferredRunDate?.toFormat(format) }) let closestScansAfterToday = scansGroupedByPreferredRunDate .filter { $0.key != nil && $0.key!.toDate(using: format) > currentDate } @@ -161,22 +175,50 @@ struct MapperToUI { .flatMap { [$0.key?.toDate(using: format): $0.value] } .first - return scanDate(element: closestScansAfterToday) + return scanDate(element: closestScansAfterToday, nearestScanOperationByBroker: nearestScanOperationByBroker) + } + + // A dictionary containing the closest scan by broker + private func nearestRunDates(for brokerData: [BrokerProfileQueryData]) -> [String: Date] { + let today = Date() + let nearestDates = brokerData.reduce(into: [String: Date]()) { result, data in + let url = data.dataBroker.url + if let operationDate = data.scanOperationData.preferredRunDate { + if operationDate > today { + if let existingDate = result[url] { + if operationDate < existingDate { + result[url] = operationDate + } + } else { + result[url] = operationDate + } + } + } + } + return nearestDates } - private func scanDate(element: Dictionary.Element?) -> DBUIScanDate { + private func scanDate(element: Dictionary.Element?, + nearestScanOperationByBroker: [String: Date]) -> DBUIScanDate { if let element = element, let date = element.key { return DBUIScanDate( date: date.timeIntervalSince1970, dataBrokers: element.value.flatMap { - var brokers = [DBPUIDataBroker(name: $0.dataBroker.name)] + let brokerOperationDate = nearestScanOperationByBroker[$0.dataBroker.url] + var brokers = [DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: brokerOperationDate?.timeIntervalSince1970 ?? nil)] for mirrorSite in $0.dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: date) { - brokers.append(DBPUIDataBroker(name: mirrorSite.name)) + brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: brokerOperationDate?.timeIntervalSince1970 ?? nil)) } return brokers } + .reduce(into: []) { result, dataBroker in // Remove dupes + guard !result.contains(where: { $0.url == dataBroker.url }) else { + return + } + result.append(dataBroker) + } ) } else { return DBUIScanDate(date: 0, dataBrokers: [DBPUIDataBroker]()) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift index 2a6108f826..a604a0a4d7 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift @@ -20,11 +20,174 @@ import XCTest @testable import DataBrokerProtection final class BrokerJSONCodableTests: XCTestCase { - let verecorJSONString = """ + let verecorWithURLJSONString = """ + { + "name": "Verecor", + "url": "verecor.com", + "version": "0.1.0", + "addedDatetime": 1677128400000, + "mirrorSites": [ + { + "name": "Potato", + "url": "potato.com", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "Tomato", + "url": "tomato.com", + "addedAt": 1705599286529, + "removedAt": null + } + ], + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "84aa05bc-1ca0-4f16-ae74-dfb352ce0eee", + "url": "https://verecor.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${ageRange}", + "ageRange": [ + "18-30", + "31-40", + "41-50", + "51-60", + "61-70", + "71-80", + "81+" + ] + }, + { + "actionType": "extract", + "id": "92252eb5-ccaf-4b00-a3fe-019110ce0534", + "selector": ".search-item", + "profile": { + "name": { + "selector": "h4" + }, + "alternativeNamesList": { + "selector": ".//div[@class='col-sm-24 col-md-16 name']//li", + "findElements": true + }, + "age": { + "selector": ".age" + }, + "addressCityStateList": { + "selector": ".//div[@class='col-sm-24 col-md-8 location']//li", + "findElements": true + }, + "profileUrl": { + "selector": "a" + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "formOptOut", + "actions": [ + { + "actionType": "navigate", + "id": "49f9aa73-4f97-47c0-b8bf-1729e9c169c0", + "url": "https://verecor.com/ng/control/privacy" + }, + { + "actionType": "fillForm", + "id": "55b1d0bb-d303-4b6f-bf9e-3fd96746f27e", + "selector": ".ahm", + "elements": [ + { + "type": "fullName", + "selector": "#user_name" + }, + { + "type": "email", + "selector": "#user_email" + }, + { + "type": "profileUrl", + "selector": "#url" + } + ] + }, + { + "actionType": "getCaptchaInfo", + "id": "9efb1153-8f52-41e4-a8fb-3077a97a586d", + "selector": ".g-recaptcha" + }, + { + "actionType": "solveCaptcha", + "id": "ed49e4c3-0cfa-4f1e-b3d1-06ad7b8b9ba4", + "selector": ".g-recaptcha" + }, + { + "actionType": "click", + "id": "6b986aa4-3d1b-44d5-8b2b-5463ee8916c9", + "elements": [ + { + "type": "button", + "selector": ".btn-sbmt" + } + ] + }, + { + "actionType": "expectation", + "id": "d4c64d9b-1004-487e-ab06-ae74869bc9a7", + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "Your removal request has been received" + } + ] + }, + { + "actionType": "emailConfirmation", + "id": "3b4c611a-61ab-4792-810e-d5b3633ea203", + "pollingTime": 30 + }, + { + "actionType": "expectation", + "id": "afe805a0-d422-473c-b47f-995a8672d476", + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "Your information control request has been confirmed." + } + ] + } + ] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } + } + + """ + let verecorNoURLJSONString = """ { "name": "verecor.com", "version": "0.1.0", "addedDatetime": 1677128400000, + "mirrorSites": [ + { + "name": "tomato.com", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "potato.com", + "addedAt": 1705599286529, + "removedAt": null + } + ], "steps": [ { "stepType": "scan", @@ -157,9 +320,27 @@ final class BrokerJSONCodableTests: XCTestCase { """ - func testVerecorJSON_isCorrectlyParsed() { + func testVerecorJSONNoURL_isCorrectlyParsed() { do { - _ = try JSONDecoder().decode(DataBroker.self, from: verecorJSONString.data(using: .utf8)!) + let broker = try JSONDecoder().decode(DataBroker.self, from: verecorNoURLJSONString.data(using: .utf8)!) + XCTAssertEqual(broker.url, broker.name) + for mirror in broker.mirrorSites { + XCTAssertEqual(mirror.url, mirror.name) + } + } catch { + XCTFail("JSON string should be parsed correctly.") + } + } + + func testVerecorJSONWithURL_isCorrectlyParsed() { + do { + let broker = try JSONDecoder().decode(DataBroker.self, from: verecorWithURLJSONString.data(using: .utf8)!) + XCTAssertEqual(broker.url, "verecor.com") + XCTAssertEqual(broker.name, "Verecor") + + for mirror in broker.mirrorSites { + XCTAssertNotEqual(mirror.url, mirror.name) + } } catch { XCTFail("JSON string should be parsed correctly.") } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index 3a65f7ae9a..5fa7f4b069 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -44,7 +44,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -92,7 +92,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -143,7 +143,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -745,7 +745,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -770,7 +770,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let currentPreferredRunDate = Date() let expectedPreferredRunDate = Date().addingTimeInterval(config.confirmOptOutScan.hoursToSeconds) - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -846,6 +846,7 @@ extension DataBroker { DataBroker( id: 1, name: "Test broker", + url: "testbroker.com", steps: [ Step(type: .scan, actions: [Action]()), Step(type: .optOut, actions: [Action]()) @@ -863,6 +864,7 @@ extension DataBroker { DataBroker( id: 1, name: "Test broker", + url: "testbroker.com", steps: [ Step(type: .scan, actions: [Action]()), Step(type: .optOut, actions: [Action](), optOutType: .parentSiteOptOut) @@ -879,6 +881,7 @@ extension DataBroker { static var mockWithoutId: DataBroker { DataBroker( name: "Test broker", + url: "testbroker.com", steps: [Step](), version: "1.0", schedulingConfig: DataBrokerScheduleConfig( diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift index dcbc31a911..2036ac9a1d 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift @@ -111,7 +111,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnOldVersionBroker = true sut.checkForUpdatesInBrokerJSONFiles() @@ -129,7 +129,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnNewVersionBroker = true sut.checkForUpdatesInBrokerJSONFiles() @@ -146,7 +146,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] vault.profileQueries = [.mock] sut.checkForUpdatesInBrokerJSONFiles() diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift index cc23589ec8..c82680d1e9 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift @@ -41,7 +41,7 @@ final class EmailServiceTests: XCTestCase { let sut = EmailService(urlSession: mockURLSession, redeemUseCase: MockRedeemUseCase()) do { - _ = try await sut.getEmail(dataBrokerName: "fakeBroker") + _ = try await sut.getEmail(dataBrokerURL: "fakeBroker") XCTFail("Expected an error to be thrown") } catch { if let error = error as? EmailError, @@ -62,7 +62,7 @@ final class EmailServiceTests: XCTestCase { let sut = EmailService(urlSession: mockURLSession, redeemUseCase: MockRedeemUseCase()) do { - _ = try await sut.getEmail(dataBrokerName: "fakeBroker") + _ = try await sut.getEmail(dataBrokerURL: "fakeBroker") XCTFail("Expected an error to be thrown") } catch { if let error = error as? EmailError, case .cantFindEmail = error { @@ -81,7 +81,7 @@ final class EmailServiceTests: XCTestCase { let sut = EmailService(urlSession: mockURLSession, redeemUseCase: MockRedeemUseCase()) do { - let email = try await sut.getEmail(dataBrokerName: "fakeBroker") + let email = try await sut.getEmail(dataBrokerURL: "fakeBroker") XCTAssertEqual("test@ddg.com", email) } catch { XCTFail("Unexpected. It should not throw") diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift index 4d1ff6b8f8..c59ee80486 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift @@ -140,9 +140,9 @@ final class MapperToUITests: XCTestCase { func testLastScans_areMappedCorrectly() { let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", lastRunDate: Date().yesterday), - .mock(dataBrokerName: "Broker #2", lastRunDate: Date().yesterday), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", lastRunDate: Date().yesterday), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", lastRunDate: Date().yesterday), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) @@ -153,9 +153,9 @@ final class MapperToUITests: XCTestCase { func testNextScans_areMappedCorrectly() { let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", preferredRunDate: Date().tomorrow), - .mock(dataBrokerName: "Broker #2", preferredRunDate: Date().tomorrow), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", preferredRunDate: Date().tomorrow), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", preferredRunDate: Date().tomorrow), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) @@ -165,7 +165,7 @@ final class MapperToUITests: XCTestCase { } func testWhenMirrorSiteIsNotInRemovedPeriod_thenItShouldBeAddedToTotalScans() { - let brokerProfileQueryWithMirrorSite: BrokerProfileQueryData = .mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: nil)]) + let brokerProfileQueryWithMirrorSite: BrokerProfileQueryData = .mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: nil)]) let brokerProfileQueryData: [BrokerProfileQueryData] = [ brokerProfileQueryWithMirrorSite, brokerProfileQueryWithMirrorSite, @@ -178,7 +178,7 @@ final class MapperToUITests: XCTestCase { } func testWhenMirrorSiteIsInRemovedPeriod_thenItShouldNotBeAddedToTotalScans() { - let brokerWithMirrorSiteThatWasRemoved = BrokerProfileQueryData.mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)]) + let brokerWithMirrorSiteThatWasRemoved = BrokerProfileQueryData.mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: Date().yesterday)]) let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(dataBrokerName: "Broker #1"), brokerWithMirrorSiteThatWasRemoved, .mock(dataBrokerName: "Broker #2")] let result = sut.initialScanState(brokerProfileQueryData) @@ -190,7 +190,7 @@ final class MapperToUITests: XCTestCase { let brokerWithMirrorSiteNotRemovedAndWithScan = BrokerProfileQueryData.mock( dataBrokerName: "Broker #1", lastRunDate: Date(), - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: nil)] + mirrorSites: [.init(name: "mirror", url: "mirror.com", addedAt: Date(), removedAt: nil)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [ brokerWithMirrorSiteNotRemovedAndWithScan, @@ -207,7 +207,7 @@ final class MapperToUITests: XCTestCase { let brokerWithMirrorSiteRemovedAndWithScan = BrokerProfileQueryData.mock( dataBrokerName: "Broker #2", lastRunDate: Date(), - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)] + mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: Date().yesterday)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [ .mock(dataBrokerName: "Broker #1"), @@ -223,7 +223,7 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteIsNotInRemovedPeriod_thenMatchIsAdded() { let brokerWithMirrorSiteNotRemovedAndWithMatch = BrokerProfileQueryData.mock( extractedProfile: .mockWithoutRemovedDate, - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: nil)] + mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: nil)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(), .mock(), brokerWithMirrorSiteNotRemovedAndWithMatch] @@ -235,7 +235,7 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteIsInRemovedPeriod_thenMatchIsNotAdded() { let brokerWithMirrorSiteRemovedAndWithMatch = BrokerProfileQueryData.mock( extractedProfile: .mockWithoutRemovedDate, - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)] + mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: Date().yesterday)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(), .mock(), brokerWithMirrorSiteRemovedAndWithMatch] @@ -246,8 +246,8 @@ final class MapperToUITests: XCTestCase { func testMirrorSites_areCorrectlyMappedToInProgressOptOuts() { let scanHistoryEventsWithMatchesFound: [HistoryEvent] = [.init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: Date())] - let mirrorSiteNotRemoved = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: nil) - let mirrorSiteRemoved = MirrorSite(name: "mirror #2", addedAt: Date.distantPast, removedAt: Date().yesterday) // Should not be added + let mirrorSiteNotRemoved = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: nil) + let mirrorSiteRemoved = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date.distantPast, removedAt: Date().yesterday) // Should not be added let brokerProfileQueryData: [BrokerProfileQueryData] = [ .mock(extractedProfile: .mockWithoutRemovedDate, scanHistoryEvents: scanHistoryEventsWithMatchesFound, @@ -261,10 +261,10 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteRemovedIsInRangeToPastRemovedProfile_thenIsAddedToCompletedOptOuts() { let scanHistoryEventsWithMatchesFound: [HistoryEvent] = [.init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: Date().yesterday!)] - let mirrorSiteRemoved = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: Date()) // Should be added + let mirrorSiteRemoved = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: Date()) // Should be added // The next two mirror sites should not be added. New mirror sites should not count for old opt-outs - let newMirrorSiteOne = MirrorSite(name: "mirror #2", addedAt: Date(), removedAt: nil) - let newMirrorSiteTwo = MirrorSite(name: "mirror #3", addedAt: Date(), removedAt: nil) + let newMirrorSiteOne = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date(), removedAt: nil) + let newMirrorSiteTwo = MirrorSite(name: "mirror #3", url: "mirror3.com", addedAt: Date(), removedAt: nil) let brokerProfileQuery = BrokerProfileQueryData.mock(extractedProfile: .mockWithRemoveDate(Date().yesterday!), scanHistoryEvents: scanHistoryEventsWithMatchesFound, mirrorSites: [mirrorSiteRemoved, newMirrorSiteOne, newMirrorSiteTwo]) @@ -276,12 +276,12 @@ final class MapperToUITests: XCTestCase { } func testLastScansWithMirrorSites_areMappedCorrectly() { - let includedMirrorSite = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: nil) - let notIncludedMirrorSite = MirrorSite(name: "mirror #2", addedAt: Date(), removedAt: nil) + let includedMirrorSite = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: nil) + let notIncludedMirrorSite = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date(), removedAt: nil) let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", lastRunDate: Date().yesterday, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), - .mock(dataBrokerName: "Broker #2", lastRunDate: Date().yesterday), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", lastRunDate: Date().yesterday, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", lastRunDate: Date().yesterday), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) @@ -291,12 +291,12 @@ final class MapperToUITests: XCTestCase { } func testNextScansWithMirrorSites_areMappedCorrectly() { - let includedMirrorSite = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: nil) - let notIncludedMirrorSite = MirrorSite(name: "mirror #2", addedAt: Date.distantPast, removedAt: Date()) + let includedMirrorSite = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: nil) + let notIncludedMirrorSite = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date.distantPast, removedAt: Date()) let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", preferredRunDate: Date().tomorrow, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), - .mock(dataBrokerName: "Broker #2", preferredRunDate: Date().tomorrow), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", preferredRunDate: Date().tomorrow, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", preferredRunDate: Date().tomorrow), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index 8585792744..12b7120077 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -152,6 +152,7 @@ extension BrokerProfileQueryData { BrokerProfileQueryData( dataBroker: DataBroker( name: "parent", + url: "parent.com", steps: [Step](), version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock @@ -165,6 +166,7 @@ extension BrokerProfileQueryData { BrokerProfileQueryData( dataBroker: DataBroker( name: "child", + url: "child.com", steps: [Step](), version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index d053404255..95ea0d1493 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -27,6 +27,7 @@ import GRDB extension BrokerProfileQueryData { static func mock(with steps: [Step] = [Step](), dataBrokerName: String = "test", + url: String = "test.com", lastRunDate: Date? = nil, preferredRunDate: Date? = nil, extractedProfile: ExtractedProfile? = nil, @@ -36,6 +37,7 @@ extension BrokerProfileQueryData { BrokerProfileQueryData( dataBroker: DataBroker( name: dataBrokerName, + url: url, steps: steps, version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, @@ -232,7 +234,7 @@ final class EmailServiceMock: EmailServiceProtocol { var shouldThrow: Bool = false - func getEmail(dataBrokerName: String?) async throws -> String { + func getEmail(dataBrokerURL: String?) async throws -> String { if shouldThrow { throw DataBrokerProtectionError.emailError(nil) } @@ -491,9 +493,9 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault func fetchBroker(with name: String) throws -> DataBroker? { if shouldReturnOldVersionBroker { - return .init(id: 1, name: "Broker", steps: [Step](), version: "1.0.0", schedulingConfig: .mock) + return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock) } else if shouldReturnNewVersionBroker { - return .init(id: 1, name: "Broker", steps: [Step](), version: "1.0.1", schedulingConfig: .mock) + return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock) } return nil diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift index b9472a9ab9..369f80ee32 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift @@ -35,6 +35,7 @@ final class OperationPreferredDateUpdaterTests: XCTestCase { let childBroker = DataBroker( id: 1, name: "Child broker", + url: "childbroker.com", steps: [Step](), version: "1.0", schedulingConfig: DataBrokerScheduleConfig( From 3002b7dfe73703eda29eb3997254afa097ff1de3 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 21 Feb 2024 18:51:13 +0000 Subject: [PATCH 05/12] Bump version to 1.76.0 (123) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 060339a249..550b71a3ee 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 122 +CURRENT_PROJECT_VERSION = 123 From 883db580de28433681a5bf97f5ddfab4600ebebe Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Wed, 21 Feb 2024 16:00:31 -0300 Subject: [PATCH 06/12] DBP: Add initial loading indicator when loading web UI (#2227) --- .../DataBrokerProtectionViewController.swift | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift index accf119bb7..ce258c3565 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift @@ -26,6 +26,7 @@ final public class DataBrokerProtectionViewController: NSViewController { private let dataManager: DataBrokerProtectionDataManaging private let scheduler: DataBrokerProtectionScheduler private var webView: WKWebView? + private var loader: NSProgressIndicator! private let webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable private let webUIViewModel: DBPUIViewModel @@ -63,9 +64,10 @@ final public class DataBrokerProtectionViewController: NSViewController { public override func viewDidLoad() { super.viewDidLoad() + addLoadingIndicator() reloadObserver = NotificationCenter.default.addObserver(forName: DataBrokerProtectionNotifications.shouldReloadUI, - object: nil, - queue: .main) { [weak self] _ in + object: nil, + queue: .main) { [weak self] _ in self?.webView?.reload() } } @@ -75,16 +77,39 @@ final public class DataBrokerProtectionViewController: NSViewController { webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 1024, height: 768), configuration: configuration) webView?.uiDelegate = self + webView?.navigationDelegate = self view = webView! if let url = URL(string: webUISettings.selectedURL) { webView?.load(url) } else { + removeLoadingIndicator() assertionFailure("Selected URL is not valid \(webUISettings.selectedURL)") } } + private func addLoadingIndicator() { + loader = NSProgressIndicator() + loader.wantsLayer = true + loader.style = .spinning + loader.controlSize = .regular + loader.sizeToFit() + loader.translatesAutoresizingMaskIntoConstraints = false + loader.controlSize = .large + view.addSubview(loader) + + NSLayoutConstraint.activate([ + loader.centerXAnchor.constraint(equalTo: view.centerXAnchor), + loader.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } + + private func removeLoadingIndicator() { + loader.stopAnimation(nil) + loader.removeFromSuperview() + } + deinit { if let reloadObserver { NotificationCenter.default.removeObserver(reloadObserver) @@ -98,3 +123,14 @@ extension DataBrokerProtectionViewController: WKUIDelegate { return nil } } + +extension DataBrokerProtectionViewController: WKNavigationDelegate { + + public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + loader.startAnimation(nil) + } + + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + removeLoadingIndicator() + } +} From d5c1756e2264d08cf308f79abe8fd22d126fccd1 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 21 Feb 2024 21:54:34 +0100 Subject: [PATCH 07/12] macOS: Transparent proxy for excluding VPN traffic. (#2128) Task/Issue URL: https://app.asana.com/0/0/1206462407536023/f Tech Design URLs: - [Tech Design: How to exclude Data Broker traffic?](https://app.asana.com/0/481882893211075/1206363506060150/f) - [Tech Design: Mechanism to allow PIR to start excluding its traffic from the VPN tunnel](https://app.asana.com/0/481882893211075/1206446978081253/f) - [Tech Design: How will the proxy recover from failure?](https://app.asana.com/0/481882893211075/1206446978546262) iOS PR: https://github.com/duckduckgo/iOS/pull/2429 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/652 ## Description Adds a transparent proxy that allows excluding app and domain traffic from the VPN. ## Known issues / limitations ### Issue 1: Exclusion delay on existing flows When switching off an exclusion, connection flows seem to switch immediately to routing through the tunnel interface again. However when turning the exclusion back ON, connection flows seem to take a bit before routing back through the proxy. This should not be a big problem as eventually connections start being excluded correctly again. It's unclear at this point if this is a macOS bug, or a bug on our proxy - but I don't think this should be a blocker by any means. --- Configuration/App/DuckDuckGoAppStore.xcconfig | 5 - Configuration/AppStore.xcconfig | 26 +- Configuration/DeveloperID.xcconfig | 14 + .../NetworkProtectionAppExtension.xcconfig | 40 +- .../VPNProxyExtension.xcconfig | 52 +++ DuckDuckGo.xcodeproj/project.pbxproj | 287 +++++++++++-- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../DBP/DataBrokerProtectionDebugMenu.swift | 10 + .../DBP/DataBrokerProtectionManager.swift | 10 +- .../DBP/LoginItem+DataBrokerProtection.swift | 1 + DuckDuckGo/DuckDuckGo.entitlements | 1 + DuckDuckGo/DuckDuckGoAppStore.entitlements | 5 + DuckDuckGo/DuckDuckGoAppStoreCI.entitlements | 4 - DuckDuckGo/DuckDuckGoDebug.entitlements | 1 + DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements | 30 -- .../DuckDuckGo_NetP_Release.entitlements | 38 -- DuckDuckGo/InfoPlist.xcstrings | 2 +- .../Bundle+VPN.swift | 65 +++ .../NetworkProtectionBundle.swift | 78 ---- .../NetworkProtectionDebugMenu.swift | 46 ++- .../NetworkProtectionTunnelController.swift | 19 +- .../MacPacketTunnelProvider.swift | 20 +- .../MacTransparentProxyProvider.swift | 94 +++++ ...NetworkProtectionAppExtension.entitlements | 1 + .../BrowserWindowManager.swift | 64 +++ .../IPCServiceManager.swift | 13 +- DuckDuckGoVPN/DuckDuckGoVPN.entitlements | 1 + DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 93 ++++- DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements | 1 + DuckDuckGoVPN/Info-AppStore.plist | 6 +- DuckDuckGoVPN/Info.plist | 6 +- DuckDuckGoVPN/VPNProxyLauncher.swift | 149 +++++++ .../DataBrokerProtection/Package.swift | 2 +- .../IPC/DataBrokerProtectionIPCClient.swift | 13 +- .../IPC/DataBrokerProtectionIPCServer.swift | 16 + .../Pixels/DataBrokerProtectionPixels.swift | 1 - LocalPackages/LoginItems/Package.swift | 2 +- .../NetworkProtectionMac/Package.resolved | 104 +++++ .../NetworkProtectionMac/Package.swift | 16 +- .../FlowManagers/TCPFlowManager.swift | 242 +++++++++++ .../FlowManagers/UDPFlowManager.swift | 329 +++++++++++++++ .../TransparentProxyAppMessageHandler.swift | 82 ++++ .../IPC/TransparentProxyRequest.swift | 67 +++ .../TransparentProxyControllerPixel.swift | 89 ++++ .../TransparentProxyProviderPixel.swift | 93 +++++ .../RoutingRules/VPNAppRoutingRules.swift | 16 +- .../RoutingRules/VPNRoutingRule.swift | 20 +- .../Settings/TransparentProxySettings.swift | 134 ++++++ .../UserDefaults+excludedApps.swift | 79 ++++ .../UserDefaults+excludedDomains.swift | 51 +++ .../TransparentProxyController.swift | 293 +++++++++++++ .../TransparentProxyProvider.swift | 389 ++++++++++++++++++ ...ransparentProxyProviderConfiguration.swift | 40 ++ .../NetworkProtectionStatusView.swift | 4 + .../NetworkProtectionStatusViewModel.swift | 34 +- ...TransparentProxyControllerPixelTests.swift | 120 ++++++ .../TransparentProxyProviderPixelTests.swift | 66 +++ LocalPackages/PixelKit/Package.swift | 2 +- .../PixelKit/PixelKit+Parameters.swift | 6 +- .../PixelKit/Sources/PixelKit/PixelKit.swift | 24 +- .../Sources/PixelKit/PixelKitEvent.swift | 2 +- .../Sources/PixelKit/PixelKitEventV2.swift | 70 ++++ .../PixelFireExpectations.swift | 36 ++ .../XCTestCase+PixelKit.swift | 148 +++++++ LocalPackages/SubscriptionUI/Package.swift | 2 +- LocalPackages/SwiftUIExtensions/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- LocalPackages/XPCHelper/Package.swift | 2 +- NetworkProtectionSystemExtension/Info.plist | 2 + ...otectionSystemExtension_Debug.entitlements | 1 + ...ectionSystemExtension_Release.entitlements | 1 + VPNProxyExtension/Info.plist | 17 + .../VPNProxyExtension.entitlements | 25 ++ fastlane/Matchfile | 2 + scripts/assets/AppStoreExportOptions.plist | 4 + 76 files changed, 3497 insertions(+), 341 deletions(-) create mode 100644 Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig delete mode 100644 DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements delete mode 100644 DuckDuckGo/DuckDuckGo_NetP_Release.entitlements create mode 100644 DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift delete mode 100644 DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift create mode 100644 DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift create mode 100644 DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift create mode 100644 DuckDuckGoVPN/VPNProxyLauncher.swift create mode 100644 LocalPackages/NetworkProtectionMac/Package.resolved create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift rename DuckDuckGoVPN/Bundle+Configuration.swift => LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift (56%) rename DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift => LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift (59%) create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift create mode 100644 LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift create mode 100644 LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift create mode 100644 LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift create mode 100644 LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift create mode 100644 LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift create mode 100644 VPNProxyExtension/Info.plist create mode 100644 VPNProxyExtension/VPNProxyExtension.entitlements diff --git a/Configuration/App/DuckDuckGoAppStore.xcconfig b/Configuration/App/DuckDuckGoAppStore.xcconfig index 3ee212ad5e..904caca8c5 100644 --- a/Configuration/App/DuckDuckGoAppStore.xcconfig +++ b/Configuration/App/DuckDuckGoAppStore.xcconfig @@ -17,11 +17,6 @@ #include "../AppStore.xcconfig" #include "ManualAppStoreRelease.xcconfig" -AGENT_BUNDLE_ID[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -AGENT_BUNDLE_ID[config=Debug][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug -AGENT_BUNDLE_ID[config=CI][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug -AGENT_BUNDLE_ID[config=Review][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.review - PRODUCT_BUNDLE_IDENTIFIER = $(MAIN_BUNDLE_IDENTIFIER) CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAppStore.entitlements diff --git a/Configuration/AppStore.xcconfig b/Configuration/AppStore.xcconfig index c2ae87c9b5..0ad3f9f6b5 100644 --- a/Configuration/AppStore.xcconfig +++ b/Configuration/AppStore.xcconfig @@ -50,21 +50,23 @@ AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review AGENT_PRODUCT_NAME = DuckDuckGo VPN App Store AGENT_RELEASE_PRODUCT_NAME = DuckDuckGo VPN -SYSEX_BUNDLE_ID[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -SYSEX_BUNDLE_ID[config=Debug][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug.network-protection-extension -SYSEX_BUNDLE_ID[config=CI][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug.network-protection-extension -SYSEX_BUNDLE_ID[config=Review][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension -SYSEX_BUNDLE_ID[config=Release][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension +// Extensions -// Distributed Notifications Prefix +PROXY_EXTENSION_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(AGENT_BUNDLE_ID).proxy + +TUNNEL_EXTENSION_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension -SYSEX_BUNDLE_ID_BASE[sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Release][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension +// Distributed Notifications Prefix -DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(SYSEX_BUNDLE_ID_BASE) +DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(AGENT_BUNDLE_ID_BASE).network-extension DISTRIBUTED_NOTIFICATIONS_PREFIX[config=CI][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).ci DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Review][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).review diff --git a/Configuration/DeveloperID.xcconfig b/Configuration/DeveloperID.xcconfig index 0bfb9bb8cb..b66acc76d2 100644 --- a/Configuration/DeveloperID.xcconfig +++ b/Configuration/DeveloperID.xcconfig @@ -65,6 +65,20 @@ AGENT_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review AGENT_PRODUCT_NAME = DuckDuckGo VPN +// Extensions + +PROXY_EXTENSION_BUNDLE_ID[sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(SYSEX_BUNDLE_ID) + +TUNNEL_EXTENSION_BUNDLE_ID[sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(SYSEX_BUNDLE_ID) + // DBP DBP_BACKGROUND_AGENT_PRODUCT_NAME = DuckDuckGo Personal Information Removal diff --git a/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig b/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig index 60ad407569..2fb095fc56 100644 --- a/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig +++ b/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig @@ -14,10 +14,7 @@ // #include "../ExtensionBase.xcconfig" - -// Since we're using nonstandard bundle IDs we'll just define them here, but we should consider -// standardizing the bundle IDs so we can just define BUNDLE_IDENTIFIER_PREFIX -BUNDLE_IDENTIFIER_PREFIX = com.duckduckgo.mobile.ios.vpn.agent +#include "../../AppStore.xcconfig" CODE_SIGN_ENTITLEMENTS[config=CI][sdk=macosx*] = CODE_SIGN_ENTITLEMENTS[config=Debug][sdk=macosx*] = DuckDuckGo/NetworkProtectionAppExtension.entitlements @@ -38,17 +35,11 @@ FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION -NETP_BASE_APP_GROUP = $(DEVELOPMENT_TEAM).com.duckduckgo.macos.browser.network-protection -NETP_APP_GROUP[config=CI][sdk=macos*] = $(NETP_BASE_APP_GROUP).debug -NETP_APP_GROUP[config=Review][sdk=macos*] = $(NETP_BASE_APP_GROUP).review -NETP_APP_GROUP[config=Debug][sdk=macos*] = $(NETP_BASE_APP_GROUP).debug -NETP_APP_GROUP[config=Release][sdk=macos*] = $(NETP_BASE_APP_GROUP) - PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = -PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).debug.network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).debug.network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).review.network-protection-extension +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) PROVISIONING_PROFILE_SPECIFIER[config=CI][sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = match AppStore com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension macos @@ -59,24 +50,3 @@ SKIP_INSTALL = YES SWIFT_EMIT_LOC_STRINGS = YES LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks @executable_path/../../../../Frameworks - -// Distributed Notifications: - -AGENT_BUNDLE_ID_BASE[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -AGENT_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID_BASE) -AGENT_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug -AGENT_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug -AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review - -SYSEX_BUNDLE_ID_BASE[sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Release][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension - -DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(SYSEX_BUNDLE_ID_BASE) - -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=CI][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).ci -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Review][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).review -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Debug][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).debug -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Release][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE) diff --git a/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig b/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig new file mode 100644 index 0000000000..5f70d87091 --- /dev/null +++ b/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig @@ -0,0 +1,52 @@ +// 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. +// + +#include "../ExtensionBase.xcconfig" +#include "../../AppStore.xcconfig" + +CODE_SIGN_ENTITLEMENTS[config=CI][sdk=macosx*] = +CODE_SIGN_ENTITLEMENTS[config=Debug][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_ENTITLEMENTS[config=Release][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_ENTITLEMENTS[config=Review][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_STYLE[config=Debug][sdk=*] = Automatic + +CODE_SIGN_IDENTITY[sdk=macosx*] = 3rd Party Mac Developer Application +CODE_SIGN_IDENTITY[config=Debug][sdk=macosx*] = Apple Development +CODE_SIGN_IDENTITY[config=CI][sdk=macosx*] = + +GENERATE_INFOPLIST_FILE = YES +INFOPLIST_FILE = VPNProxyExtension/Info.plist +INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023 DuckDuckGo. All rights reserved. + +FEATURE_FLAGS[arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION + +PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) + +PROVISIONING_PROFILE_SPECIFIER[config=CI][sdk=macosx*] = +PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = match AppStore $(AGENT_BUNDLE_ID).proxy macos +PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = match AppStore $(AGENT_BUNDLE_ID).proxy macos + +SDKROOT = macosx +SKIP_INSTALL = YES +SWIFT_EMIT_LOC_STRINGS = YES + +LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks @executable_path/../../../../Frameworks diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0f1e8a2d3e..0a586a3376 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1099,12 +1099,11 @@ 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B2537722A11BF8B00610219 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B25376F2A11BF8B00610219 /* main.swift */; }; 4B2537772A11BFE100610219 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2537762A11BFE100610219 /* PixelKit */; }; - 4B2537782A11C00F00610219 /* NetworkProtectionExtensionMachService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */; }; 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B29759728281F0900187C4E /* FirefoxEncryptionKeyReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B29759628281F0900187C4E /* FirefoxEncryptionKeyReader.swift */; }; 4B2975992828285900187C4E /* FirefoxKeyReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2975982828285900187C4E /* FirefoxKeyReaderTests.swift */; }; 4B2AAAF529E70DEA0026AFC0 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2AAAF429E70DEA0026AFC0 /* Lottie */; }; - 4B2D06292A11C0C900DE1F49 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2D062B2A11C0E100DE1F49 /* Networking */; }; 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; @@ -1168,17 +1167,16 @@ 4B44FEF52B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B44FEF22B1FEF5A000619D8 /* FocusableTextEditor.swift */; }; 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC382A11B509001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift */; }; 4B4BEC3E2A11B56E001D9AC5 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC322A11B509001D9AC5 /* Logging.swift */; }; - 4B4BEC402A11B5B5001D9AC5 /* NetworkProtectionExtensionMachService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */; }; 4B4BEC412A11B5BD001D9AC5 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */; }; 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; - 4B4BEC432A11B5C7001D9AC5 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B4BEC482A11B61F001D9AC5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B4BEC342A11B509001D9AC5 /* Assets.xcassets */; }; 4B4D603F2A0B290200BCD287 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */; }; 4B4D60982A0B2A5C00BCD287 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B4D60972A0B2A5C00BCD287 /* PixelKit */; }; 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; - 4B4D60A02A0B2D5B00BCD287 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; @@ -1202,7 +1200,7 @@ 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 4B4D60DD2A0C875E00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B4D60DF2A0C875F00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; - 4B4D60E02A0C875F00BCD287 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4D60E32A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */; }; @@ -1528,7 +1526,7 @@ 4B957A422AC7AE700062CA31 /* SafariFaviconsReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0AACAD28BC6FD0001038AC /* SafariFaviconsReader.swift */; }; 4B957A432AC7AE700062CA31 /* NSScreenExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */; }; 4B957A442AC7AE700062CA31 /* NSBezierPathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65E6B9F26D9F10600095F96 /* NSBezierPathExtension.swift */; }; - 4B957A452AC7AE700062CA31 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B957A452AC7AE700062CA31 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B957A462AC7AE700062CA31 /* WebsiteDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6820E325502F19005ED0D5 /* WebsiteDataStore.swift */; }; 4B957A472AC7AE700062CA31 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; 4B957A482AC7AE700062CA31 /* PermissionContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C852926942AC90048FEBE /* PermissionContextMenu.swift */; }; @@ -2049,7 +2047,7 @@ 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8F52402A18326600BE7131 /* NetworkProtectionTunnelController.swift */; }; 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 4BA7C4DD2B3F64E500AFE511 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 4BA7C4DC2B3F64E500AFE511 /* LoginItems */; }; - 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in Embed Network Extensions */ = {isa = PBXBuildFile; fileRef = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4BB6CE5F26B77ED000EC5860 /* Cryptography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB6CE5E26B77ED000EC5860 /* Cryptography.swift */; }; 4BB88B4525B7B55C006F6B06 /* DebugUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4425B7B55C006F6B06 /* DebugUserScript.swift */; }; 4BB88B4A25B7B690006F6B06 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; @@ -2133,7 +2131,7 @@ 4BF97AD62B43C45800EB4240 /* NetworkProtectionNavBarPopoverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3618C12ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift */; }; 4BF97AD72B43C53D00EB4240 /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; 4BF97AD82B43C5B300EB4240 /* NetworkProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */; }; - 4BF97AD92B43C5C000EB4240 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4BF97ADA2B43C5DC00EB4240 /* VPNFeedbackCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526632B1D55D80054955A /* VPNFeedbackCategory.swift */; }; 4BF97ADB2B43C5E000EB4240 /* VPNMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B05265D2B1AE5C70054955A /* VPNMetadataCollector.swift */; }; 4BF97ADC2B43C5E200EB4240 /* VPNFeedbackSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526602B1D55320054955A /* VPNFeedbackSender.swift */; }; @@ -2174,6 +2172,14 @@ 56D145F229E6F06D00E3488A /* MockBookmarkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */; }; 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; 56D6A3D729DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; + 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; + 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */; }; + 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */; }; + 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; + 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; + 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; + 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; + 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */; }; 7B1E819E27C8874900FF0E60 /* ContentOverlayPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */; }; 7B1E819F27C8874900FF0E60 /* ContentOverlay.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */; }; 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */; }; @@ -2190,15 +2196,25 @@ 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4CE8E626F02134009134B1 /* TabBarTests.swift */; }; 7B5DD69A2AE51FFA001DE99C /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B5DD6992AE51FFA001DE99C /* PixelKit */; }; 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B5F9A742AE2BE4E002AEBC0 /* PixelKit */; }; + 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; + 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7B7DFB212B7E7473009EA1A3 /* Networking */; }; 7B8C083C2AE1268E00F4C67F /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B8C083B2AE1268E00F4C67F /* PixelKit */; }; 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */; }; 7B934C412A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; + 7B94E1652B7ED95100E32B96 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */; }; + 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */; }; + 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD5A2B7E0B85004FEF43 /* Common */; }; + 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; + 7B97CD5D2B7E0BCE004FEF43 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; + 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; + 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; + 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; + 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD612B7E0C4B004FEF43 /* PixelKit */; }; + 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */; }; 7BA4727D26F01BC400EAA165 /* CoreDataTestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C42667104B00AD2C21 /* CoreDataTestUtilities.swift */; }; 7BA59C9B2AE18B49009A97B1 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */; }; 7BA7CC392AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */; }; 7BA7CC3A2AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */; }; - 7BA7CC3B2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */; }; - 7BA7CC3C2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */; }; 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */; }; 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */; }; 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */; }; @@ -2214,8 +2230,8 @@ 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606A2A0B29FA00BCD287 /* NetworkProtectionControllerErrorStore.swift */; }; 7BA7CC4E2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; 7BA7CC502AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; - 7BA7CC532AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; - 7BA7CC542AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; + 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 7BA7CC552AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; @@ -2233,9 +2249,13 @@ 7BBD44282AD730A400D0A064 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBD44272AD730A400D0A064 /* PixelKit */; }; 7BBD45B12A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBD45B22A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; + 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */; }; 7BD01C192AD8319C0088B32E /* IPCServiceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */; }; 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */; }; + 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; + 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; + 7BDA36F92B7E084A00AD5388 /* VPNProxyExtension.appex in Embed Network Extensions */ = {isa = PBXBuildFile; fileRef = 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; 7BE146082A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; 7BEC182F2AD5D8DC00D30536 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */; }; @@ -3104,7 +3124,6 @@ EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; EECE10E529DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; EECE10E629DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; - EEF12E6E2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; F41D174125CB131900472416 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; @@ -3185,6 +3204,13 @@ remoteGlobalIDString = AA585D7D248FD31100E9A3E2; remoteInfo = "DuckDuckGo Privacy Browser"; }; + 7BDA36F72B7E082100AD5388 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AA585D76248FD31100E9A3E2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7BDA36E42B7E037100AD5388; + remoteInfo = VPNProxyExtension; + }; 7BEC18302AD5DA3300D30536 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AA585D76248FD31100E9A3E2 /* Project object */; @@ -3242,14 +3268,16 @@ name = "Embed Login Items"; runOnlyForDeploymentPostprocessing = 0; }; - 4BA7C4E02B3F6F7500AFE511 /* CopyFiles */ = { + 4BA7C4E02B3F6F7500AFE511 /* Embed Network Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( - 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in CopyFiles */, + 7BDA36F92B7E084A00AD5388 /* VPNProxyExtension.appex in Embed Network Extensions */, + 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in Embed Network Extensions */, ); + name = "Embed Network Extensions"; runOnlyForDeploymentPostprocessing = 0; }; B6EC37E629B5DA2A001ACE79 /* CopyFiles */ = { @@ -3546,7 +3574,7 @@ 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetworkProtectionSystemExtension.xcconfig; sourceTree = ""; }; 4B4D60502A0B293C00BCD287 /* NetworkProtectionAppExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetworkProtectionAppExtension.xcconfig; sourceTree = ""; }; 4B4D60512A0B293C00BCD287 /* ExtensionBase.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ExtensionBase.xcconfig; sourceTree = ""; }; - 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionBundle.swift; sourceTree = ""; }; + 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+VPN.swift"; sourceTree = ""; }; 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOptionKeyExtension.swift; sourceTree = ""; }; 4B4D60652A0B29FA00BCD287 /* NetworkProtectionNavBarButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarButtonModel.swift; sourceTree = ""; }; 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+ConvenienceInitializers.swift"; sourceTree = ""; }; @@ -3556,10 +3584,7 @@ 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteCodeViewModel.swift; sourceTree = ""; }; 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventMapping+NetworkProtectionError.swift"; sourceTree = ""; }; 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationsPresenter.swift; sourceTree = ""; }; - 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionExtensionMachService.swift; sourceTree = ""; }; 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtectionExtensions.swift"; sourceTree = ""; }; - 4B4D609C2A0B2C2300BCD287 /* DuckDuckGo_NetP_Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGo_NetP_Release.entitlements; sourceTree = ""; }; - 4B4D609E2A0B2C2300BCD287 /* DuckDuckGo_NetP_Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGo_NetP_Debug.entitlements; sourceTree = ""; }; 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtection.swift"; sourceTree = ""; }; 4B4D60E12A0C883A00BCD287 /* AppMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMain.swift; sourceTree = ""; }; 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; @@ -3766,7 +3791,10 @@ 56D145ED29E6DAD900E3488A /* DataImportProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportProviderTests.swift; sourceTree = ""; }; 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBookmarkManager.swift; sourceTree = ""; }; 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueSetUpView.swift; sourceTree = ""; }; + 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowManager.swift; sourceTree = ""; }; + 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTransparentProxyProvider.swift; sourceTree = ""; }; 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; + 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNProxyLauncher.swift; sourceTree = ""; }; 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayPopover.swift; sourceTree = ""; }; 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ContentOverlay.storyboard; sourceTree = ""; }; 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayViewController.swift; sourceTree = ""; }; @@ -3790,7 +3818,6 @@ 7BA7CC0B2AD11D1E0042E5CE /* DuckDuckGoVPNAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPNAppStore.xcconfig; sourceTree = ""; }; 7BA7CC0C2AD11D1E0042E5CE /* DuckDuckGoVPN.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPN.xcconfig; sourceTree = ""; }; 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckDuckGoVPNAppDelegate.swift; sourceTree = ""; }; - 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Configuration.swift"; sourceTree = ""; }; 7BA7CC102AD11DC80042E5CE /* Info-AppStore.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-AppStore.plist"; sourceTree = ""; }; 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelControllerIPCService.swift; sourceTree = ""; }; 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -3809,6 +3836,10 @@ 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionController.swift; sourceTree = ""; }; 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainType+ClientDefault.swift"; sourceTree = ""; }; 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibility.swift; sourceTree = ""; }; + 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = VPNProxyExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7BDA36EA2B7E037200AD5388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VPNProxyExtension.entitlements; sourceTree = ""; }; + 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = VPNProxyExtension.xcconfig; sourceTree = ""; }; 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugMenu.swift; sourceTree = ""; }; 7BEC182D2AD5D89C00D30536 /* SystemExtensionManager */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SystemExtensionManager; sourceTree = ""; }; 7BEC20402B0F505F00243D3E /* AddBookmarkPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkPopoverView.swift; sourceTree = ""; }; @@ -4443,6 +4474,7 @@ 373FB4B32B4D6C4B004C88D6 /* PreferencesViews in Frameworks */, 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */, 4BF97AD32B43C43F00EB4240 /* NetworkProtectionUI in Frameworks */, + 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */, B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */, 984FD3BF299ACF35007334DD /* Bookmarks in Frameworks */, 37A5E2F0298AA1B20047046B /* Persistence in Frameworks */, @@ -4510,6 +4542,7 @@ 37269F012B332FC8005E8E46 /* Common in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, 4B2537772A11BFE100610219 /* PixelKit in Frameworks */, + 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */, 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */, 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */, ); @@ -4520,6 +4553,7 @@ buildActionMask = 2147483647; files = ( 4B41EDAB2B1544B2001EEDF4 /* LoginItems in Frameworks */, + 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */, 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */, 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */, 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */, @@ -4536,6 +4570,7 @@ 7BFCB7502ADE7E2300DA3EA7 /* PixelKit in Frameworks */, 7BA7CC612AD1211C0042E5CE /* Networking in Frameworks */, 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */, + 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */, EE7295EF2A545C12008C0991 /* NetworkProtection in Frameworks */, 4B2D067F2A1334D700DE1F49 /* NetworkProtectionUI in Frameworks */, 4BA7C4DD2B3F64E500AFE511 /* LoginItems in Frameworks */, @@ -4571,6 +4606,7 @@ 3143C8792B0D1F3D00382627 /* DataBrokerProtection in Frameworks */, 372217842B33380E00B8E9C2 /* TestUtils in Frameworks */, 4B957BD62AC7AE700062CA31 /* LoginItems in Frameworks */, + 7B94E1652B7ED95100E32B96 /* NetworkProtectionProxy in Frameworks */, 4B957BD72AC7AE700062CA31 /* NetworkProtection in Frameworks */, 4B957BD82AC7AE700062CA31 /* BrowserServicesKit in Frameworks */, 4B957BDA2AC7AE700062CA31 /* Bookmarks in Frameworks */, @@ -4613,6 +4649,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E22B7E037100AD5388 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */, + 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */, + 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */, + 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */, + 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8C62AAA39A70026E7DC /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -4661,6 +4709,7 @@ 37DF000529F9C056002B7D3E /* SyncDataProviders in Frameworks */, 37BA812D29B3CD690053F1A3 /* SyncUI in Frameworks */, 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */, + 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */, 4B4D60B12A0C83B900BCD287 /* NetworkProtectionUI in Frameworks */, 98A50964294B691800D10880 /* Persistence in Frameworks */, ); @@ -5240,8 +5289,9 @@ 4B18E32C2A1ECF1F005D0AAA /* NetworkProtection */ = { isa = PBXGroup; children = ( - 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */, 4B4D60502A0B293C00BCD287 /* NetworkProtectionAppExtension.xcconfig */, + 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */, + 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */, ); path = NetworkProtection; sourceTree = ""; @@ -5379,7 +5429,7 @@ 4B4D605D2A0B29FA00BCD287 /* AppAndExtensionAndNotificationTargets */ = { isa = PBXGroup; children = ( - 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */, + 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */, 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */, B602E8152A1E2570006D261F /* URL+NetworkProtection.swift */, ); @@ -5463,7 +5513,6 @@ isa = PBXGroup; children = ( B602E81F2A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift */, - 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */, ); path = SystemExtensionAndNotificationTargets; sourceTree = ""; @@ -5481,6 +5530,7 @@ children = ( 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */, EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, + 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */, ); path = NetworkExtensionTargets; sourceTree = ""; @@ -6126,11 +6176,11 @@ isa = PBXGroup; children = ( 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */, - 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */, 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */, 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */, 7BA7CC152AD11DC80042E5CE /* NetworkProtectionBouncer.swift */, 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */, + 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */, 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */, 7BA7CC172AD11DC80042E5CE /* UserText.swift */, 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */, @@ -6160,6 +6210,15 @@ path = LetsMove1.25; sourceTree = ""; }; + 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */ = { + isa = PBXGroup; + children = ( + 7BDA36EA2B7E037200AD5388 /* Info.plist */, + 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */, + ); + path = VPNProxyExtension; + sourceTree = ""; + }; 853014D425E6709500FB8205 /* Support */ = { isa = PBXGroup; children = ( @@ -6523,6 +6582,7 @@ 9D9AE9152AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift */, 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */, 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */, + 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */, 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */, 9D9AE9172AAA3B450026E7DC /* UserText.swift */, 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */, @@ -6646,6 +6706,7 @@ B6EC37E929B5DA2A001ACE79 /* tests-server */, 7B96D0D02ADFDA7F007E02C8 /* DuckDuckGoDBPTests */, 4B5F14F72A148B230060320F /* NetworkProtectionAppExtension */, + 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */, 4B25375C2A11BE7500610219 /* NetworkProtectionSystemExtension */, 9D9AE9132AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgent */, 7BA7CC0D2AD11DC80042E5CE /* DuckDuckGoVPN */, @@ -6676,6 +6737,7 @@ 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */, 565E46DD2B2725DC0013AC2A /* SyncE2EUITests.xctest */, 376113D42B29CD5B00E794BB /* SyncE2EUITests App Store.xctest */, + 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */, ); name = Products; sourceTree = ""; @@ -6746,8 +6808,6 @@ 4B5F15032A1570F10060320F /* DuckDuckGoDebug.entitlements */, 37D9BBA329376EE8000B99F9 /* DuckDuckGoAppStore.entitlements */, 377E54382937B7C400780A0A /* DuckDuckGoAppStoreCI.entitlements */, - 4B4D609E2A0B2C2300BCD287 /* DuckDuckGo_NetP_Debug.entitlements */, - 4B4D609C2A0B2C2300BCD287 /* DuckDuckGo_NetP_Release.entitlements */, 4B2D06642A132F3A00DE1F49 /* NetworkProtectionAppExtension.entitlements */, 4B5F14C42A145D6A0060320F /* NetworkProtectionVPNController.entitlements */, 56CEE9092B7A66C500CF10AA /* Info.plist */, @@ -8280,6 +8340,7 @@ 4BF97AD42B43C43F00EB4240 /* NetworkProtection */, 373FB4B22B4D6C4B004C88D6 /* PreferencesViews */, 312978892B64131200B67619 /* DataBrokerProtection */, + 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8395,6 +8456,7 @@ 4B2D062B2A11C0E100DE1F49 /* Networking */, EE7295E82A545BC4008C0991 /* NetworkProtection */, 37269F002B332FC8005E8E46 /* Common */, + 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -8425,6 +8487,7 @@ 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */, 7BFCB74D2ADE7E1A00DA3EA7 /* PixelKit */, 4B41EDAA2B1544B2001EEDF4 /* LoginItems */, + 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */, ); productName = DuckDuckGoAgent; productReference = 4B2D06392A11CFBB00DE1F49 /* DuckDuckGo VPN.app */; @@ -8438,11 +8501,12 @@ 4B2D06662A13318400DE1F49 /* Frameworks */, 4B2D06672A13318400DE1F49 /* Resources */, 4B2D067D2A13341200DE1F49 /* ShellScript */, - 4BA7C4E02B3F6F7500AFE511 /* CopyFiles */, + 4BA7C4E02B3F6F7500AFE511 /* Embed Network Extensions */, ); buildRules = ( ); dependencies = ( + 7BDA36F82B7E082100AD5388 /* PBXTargetDependency */, 4BA7C4DF2B3F6F4900AFE511 /* PBXTargetDependency */, B6080BA52B20AF8800B418EF /* PBXTargetDependency */, ); @@ -8453,6 +8517,7 @@ 7BA7CC602AD1211C0042E5CE /* Networking */, 7BEEA5132AD1236300A9E72B /* NetworkProtectionIPC */, 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */, + 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */, 4BA7C4DC2B3F64E500AFE511 /* LoginItems */, ); productName = DuckDuckGoAgentAppStore; @@ -8552,6 +8617,7 @@ 372217832B33380E00B8E9C2 /* TestUtils */, 373FB4B42B4D6C57004C88D6 /* PreferencesViews */, 1E21F8E22B73E48600FB272E /* Subscription */, + 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */; @@ -8596,6 +8662,29 @@ productReference = 7B4CE8DA26F02108009134B1 /* UI Tests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7BDA36F42B7E037200AD5388 /* Build configuration list for PBXNativeTarget "VPNProxyExtension" */; + buildPhases = ( + 7BDA36E12B7E037100AD5388 /* Sources */, + 7BDA36E22B7E037100AD5388 /* Frameworks */, + 7BDA36E32B7E037100AD5388 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = VPNProxyExtension; + packageProductDependencies = ( + 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */, + 7B97CD5A2B7E0B85004FEF43 /* Common */, + 7B97CD612B7E0C4B004FEF43 /* PixelKit */, + 7B7DFB212B7E7473009EA1A3 /* Networking */, + ); + productName = VPNProxyExtension; + productReference = 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 9D9AE8B22AAA39A70026E7DC /* DuckDuckGoDBPBackgroundAgent */ = { isa = PBXNativeTarget; buildConfigurationList = 9D9AE8CC2AAA39A70026E7DC /* Build configuration list for PBXNativeTarget "DuckDuckGoDBPBackgroundAgent" */; @@ -8690,6 +8779,7 @@ 37269EFA2B332F9E005E8E46 /* Common */, 3722177F2B3337FE00B8E9C2 /* TestUtils */, 373FB4B02B4D6C42004C88D6 /* PreferencesViews */, + 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -8746,7 +8836,7 @@ AA585D76248FD31100E9A3E2 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1500; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1400; ORGANIZATIONNAME = DuckDuckGo; TargetAttributes = { @@ -8788,6 +8878,9 @@ CreatedOnToolsVersion = 12.5.1; TestTargetID = AA585D7D248FD31100E9A3E2; }; + 7BDA36E42B7E037100AD5388 = { + CreatedOnToolsVersion = 15.2; + }; AA585D7D248FD31100E9A3E2 = { CreatedOnToolsVersion = 11.5; }; @@ -8833,6 +8926,7 @@ 3706FE9B293F662100E42796 /* Integration Tests App Store */, B6EC37E729B5DA2A001ACE79 /* tests-server */, 4B4D603C2A0B290200BCD287 /* NetworkProtectionAppExtension */, + 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */, 4B2537592A11BE7300610219 /* NetworkProtectionSystemExtension */, 4B4BEC1F2A11B4E2001D9AC5 /* DuckDuckGoNotifications */, 4B2D06382A11CFBA00DE1F49 /* DuckDuckGoVPN */, @@ -9050,6 +9144,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E32B7E037100AD5388 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8C92AAA39A70026E7DC /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -9861,7 +9962,7 @@ B66260E829ACD0C900E9E3EE /* DuckPlayerTabExtension.swift in Sources */, 3706FBAA293F65D500E42796 /* HoverUserScript.swift in Sources */, 3706FBAC293F65D500E42796 /* MainMenuActions.swift in Sources */, - 4BF97AD92B43C5C000EB4240 /* NetworkProtectionBundle.swift in Sources */, + 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */, 3706FBAE293F65D500E42796 /* DataImport.swift in Sources */, 3706FBAF293F65D500E42796 /* FireproofDomains.xcdatamodeld in Sources */, B626A7552991413000053070 /* SerpHeadersNavigationResponder.swift in Sources */, @@ -10520,11 +10621,11 @@ 4B2D06332A11C1E300DE1F49 /* OptionalExtension.swift in Sources */, 4BF0E50B2AD2552200FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B41EDA12B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, - 4B2537782A11C00F00610219 /* NetworkProtectionExtensionMachService.swift in Sources */, + 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */, B65DA5F32A77D3C700CBEE8D /* UserDefaultsWrapper.swift in Sources */, 4B2537722A11BF8B00610219 /* main.swift in Sources */, EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, - 4B2D06292A11C0C900DE1F49 /* NetworkProtectionBundle.swift in Sources */, + 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10543,14 +10644,14 @@ 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, B6F92BAC2A6937B3002ABA6B /* OptionalExtension.swift in Sources */, 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */, - 7BA7CC532AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */, - 7BA7CC3C2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */, + 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, 7BFE95562A9DF29B0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, + 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */, B65DA5EF2A77CC3A00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, 4BF0E5072AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, @@ -10569,10 +10670,10 @@ B6F92BA32A691583002ABA6B /* UserDefaultsWrapper.swift in Sources */, 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */, 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, + 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, 7BA7CC5A2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */, - 7BA7CC3B2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */, EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */, 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 4B0EF7292B5780EB009D6481 /* VPNAppEventsHandler.swift in Sources */, @@ -10590,7 +10691,7 @@ 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */, 7BA7CC432AD11E480042E5CE /* UserText.swift in Sources */, - 7BA7CC542AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */, + 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10601,12 +10702,11 @@ 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */, 4B4BEC3E2A11B56E001D9AC5 /* Logging.swift in Sources */, 4B4BEC412A11B5BD001D9AC5 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, - 4B4BEC432A11B5C7001D9AC5 /* NetworkProtectionBundle.swift in Sources */, + 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */, 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */, 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8222A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, B602E81A2A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, - 4B4BEC402A11B5B5001D9AC5 /* NetworkProtectionExtensionMachService.swift in Sources */, EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -10617,14 +10717,14 @@ files = ( 4B41EDA02B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, + 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, B65DA5F52A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, - 4B4D60A02A0B2D5B00BCD287 /* NetworkProtectionBundle.swift in Sources */, + 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */, 4B4D60AD2A0C807300BCD287 /* NSApplicationExtension.swift in Sources */, 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */, - EEF12E6E2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, 4BF0E50C2AD2552300FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */, B65DA5F22A77D3C600CBEE8D /* UserDefaultsWrapper.swift in Sources */, @@ -10909,7 +11009,7 @@ 4B957A422AC7AE700062CA31 /* SafariFaviconsReader.swift in Sources */, 4B957A432AC7AE700062CA31 /* NSScreenExtension.swift in Sources */, 4B957A442AC7AE700062CA31 /* NSBezierPathExtension.swift in Sources */, - 4B957A452AC7AE700062CA31 /* NetworkProtectionBundle.swift in Sources */, + 4B957A452AC7AE700062CA31 /* Bundle+VPN.swift in Sources */, B68D21CA2ACBC971002DA3C2 /* MockPrivacyConfiguration.swift in Sources */, 4B957A462AC7AE700062CA31 /* WebsiteDataStore.swift in Sources */, 4B957A472AC7AE700062CA31 /* NetworkProtectionFeatureVisibility.swift in Sources */, @@ -11378,11 +11478,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E12B7E037100AD5388 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */, + 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */, + 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */, + 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */, + 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */, + 7B97CD5D2B7E0BCE004FEF43 /* BundleExtension.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8B62AAA39A70026E7DC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, + 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, @@ -11394,6 +11508,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, @@ -11677,7 +11792,7 @@ 4B0AACAE28BC6FD0001038AC /* SafariFaviconsReader.swift in Sources */, B6B3E0E12657EA7A0040E0A2 /* NSScreenExtension.swift in Sources */, B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, - 4B4D60E02A0C875F00BCD287 /* NetworkProtectionBundle.swift in Sources */, + 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, @@ -12449,6 +12564,11 @@ target = AA585D7D248FD31100E9A3E2 /* DuckDuckGo Privacy Browser */; targetProxy = 7B4CE8DF26F02108009134B1 /* PBXContainerItemProxy */; }; + 7BDA36F82B7E082100AD5388 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */; + targetProxy = 7BDA36F72B7E082100AD5388 /* PBXContainerItemProxy */; + }; 7BEC18312AD5DA3300D30536 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4B2537592A11BE7300610219 /* NetworkProtectionSystemExtension */; @@ -12918,6 +13038,34 @@ }; name = Release; }; + 7BDA36F02B7E037200AD5388 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 7BDA36F12B7E037200AD5388 /* CI */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = CI; + }; + 7BDA36F22B7E037200AD5388 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 7BDA36F32B7E037200AD5388 /* Review */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Review; + }; 9D9AE8CD2AAA39A70026E7DC /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7B6EC5E52AE2D8AF004FE6DF /* DuckDuckGoDBPAgent.xcconfig */; @@ -13225,6 +13373,17 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7BDA36F42B7E037200AD5388 /* Build configuration list for PBXNativeTarget "VPNProxyExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7BDA36F02B7E037200AD5388 /* Debug */, + 7BDA36F12B7E037200AD5388 /* CI */, + 7BDA36F22B7E037200AD5388 /* Release */, + 7BDA36F32B7E037200AD5388 /* Review */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 9D9AE8CC2AAA39A70026E7DC /* Build configuration list for PBXNativeTarget "DuckDuckGoDBPBackgroundAgent" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -13379,7 +13538,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 109.0.0; + version = 109.0.1; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -13768,6 +13927,18 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = NetworkProtection; }; + 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7B31FD8B2AD125620086AA24 /* NetworkProtectionIPC */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionIPC; @@ -13784,10 +13955,36 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7B7DFB212B7E7473009EA1A3 /* Networking */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Networking; + }; 7B8C083B2AE1268E00F4C67F /* PixelKit */ = { isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B97CD5A2B7E0B85004FEF43 /* Common */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Common; + }; + 7B97CD612B7E0C4B004FEF43 /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + productName = PixelKit; + }; + 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */ = { isa = XCSwiftPackageProductDependency; productName = SystemExtensionManager; @@ -13806,6 +14003,10 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */ = { isa = XCSwiftPackageProductDependency; productName = SystemExtensionManager; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4796599b2d..e0a1c42442 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" : "5ecf4fe56f334be6eaecb65f6d55632a6d53921c", - "version" : "109.0.0" + "revision" : "da6a822844922401d80e26963b8b11dcd6ef221a", + "version" : "109.0.1" } }, { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index ebe3179a76..2f5d747ade 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -23,6 +23,7 @@ import Foundation import AppKit import Common import LoginItems +import NetworkProtectionProxy @MainActor final class DataBrokerProtectionDebugMenu: NSMenu { @@ -82,6 +83,11 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem(title: "Restart", action: #selector(DataBrokerProtectionDebugMenu.backgroundAgentRestart)) .targetting(self) + + NSMenuItem.separator() + + NSMenuItem(title: "Show agent IP address", action: #selector(DataBrokerProtectionDebugMenu.showAgentIPAddress)) + .targetting(self) } NSMenuItem(title: "Operations") { @@ -253,6 +259,10 @@ final class DataBrokerProtectionDebugMenu: NSMenu { window.delegate = self } + @objc private func showAgentIPAddress() { + DataBrokerProtectionManager.shared.showAgentIPAddress() + } + @objc private func showForceOptOutWindow() { let viewController = DataBrokerForceOptOutViewController() let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index f2c9bebd4d..75f8c3e855 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -41,8 +41,10 @@ public final class DataBrokerProtectionManager { return dataManager }() + private lazy var ipcClient = DataBrokerProtectionIPCClient(machServiceName: Bundle.main.dbpBackgroundAgentBundleId, pixelHandler: pixelHandler) + lazy var scheduler: DataBrokerProtectionLoginItemScheduler = { - let ipcClient = DataBrokerProtectionIPCClient(machServiceName: Bundle.main.dbpBackgroundAgentBundleId, pixelHandler: pixelHandler) + let ipcScheduler = DataBrokerProtectionIPCScheduler(ipcClient: ipcClient) return DataBrokerProtectionLoginItemScheduler(ipcScheduler: ipcScheduler, pixelHandler: pixelHandler) @@ -57,6 +59,12 @@ public final class DataBrokerProtectionManager { public func shouldAskForInviteCode() -> Bool { redeemUseCase.shouldAskForInviteCode() } + + // MARK: - Debugging Features + + public func showAgentIPAddress() { + ipcClient.openBrowser(domain: "https://www.whatismyip.com") + } } extension DataBrokerProtectionManager: DataBrokerProtectionDataManagerDelegate { diff --git a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift index e1e00e38c7..cdbfa623a9 100644 --- a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift +++ b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Foundation import LoginItems #if DBP diff --git a/DuckDuckGo/DuckDuckGo.entitlements b/DuckDuckGo/DuckDuckGo.entitlements index 7b79b8b2fe..757dc88e2c 100644 --- a/DuckDuckGo/DuckDuckGo.entitlements +++ b/DuckDuckGo/DuckDuckGo.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.developer.system-extension.install diff --git a/DuckDuckGo/DuckDuckGoAppStore.entitlements b/DuckDuckGo/DuckDuckGoAppStore.entitlements index e419bc0920..97443cb452 100644 --- a/DuckDuckGo/DuckDuckGoAppStore.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStore.entitlements @@ -19,6 +19,11 @@ com.apple.security.files.user-selected.read-write + com.apple.developer.networking.networkextension + + packet-tunnel-provider + app-proxy-provider + com.apple.security.network.client com.apple.security.personal-information.location diff --git a/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements b/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements index a2c7bd6bd5..13ea43d233 100644 --- a/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements @@ -2,10 +2,6 @@ - com.apple.developer.networking.networkextension - - packet-tunnel-provider - com.apple.security.app-sandbox com.apple.security.application-groups diff --git a/DuckDuckGo/DuckDuckGoDebug.entitlements b/DuckDuckGo/DuckDuckGoDebug.entitlements index dcffb16791..dad1686cba 100644 --- a/DuckDuckGo/DuckDuckGoDebug.entitlements +++ b/DuckDuckGo/DuckDuckGoDebug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.developer.system-extension.install diff --git a/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements b/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements deleted file mode 100644 index 069c866e05..0000000000 --- a/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements +++ /dev/null @@ -1,30 +0,0 @@ - - - - - com.apple.developer.networking.networkextension - - packet-tunnel-provider - - com.apple.developer.system-extension.install - - com.apple.developer.usernotifications.time-sensitive - - com.apple.security.application-groups - - HKE973VLUW.com.duckduckgo.network-protection - $(NETP_APP_GROUP) - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.personal-information.location - - keychain-access-groups - - $(AppIdentifierPrefix)com.duckduckgo.macos.browser - $(AppIdentifierPrefix)com.duckduckgo.network-protection - - - diff --git a/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements b/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements deleted file mode 100644 index a2226d1f8d..0000000000 --- a/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements +++ /dev/null @@ -1,38 +0,0 @@ - - - - - com.apple.developer.networking.networkextension - - packet-tunnel-provider-systemextension - - com.apple.developer.system-extension.install - - com.apple.developer.usernotifications.time-sensitive - - com.apple.security.application-groups - - $(TeamIdentifierPrefix)com.duckduckgo.macos.browser.network-protection - $(NETP_APP_GROUP) - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.personal-information.location - - keychain-access-groups - - $(AppIdentifierPrefix)com.duckduckgo.macos.browser - $(AppIdentifierPrefix)com.duckduckgo.network-protection - - com.apple.security.personal-information.location - - com.apple.developer.networking.networkextension - - packet-tunnel-provider-systemextension - - com.apple.developer.system-extension.install - - - diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index 4d7c2d94c0..70d7389fb7 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -8,7 +8,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "DuckDuckGo" + "value" : "DuckDuckGo Privacy Pro" } } } diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift new file mode 100644 index 0000000000..169f0ceb50 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift @@ -0,0 +1,65 @@ +// +// Bundle+VPN.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 +import NetworkProtection + +extension Bundle { + + private enum VPNInfoKey: String { + case tunnelExtensionBundleID = "TUNNEL_EXTENSION_BUNDLE_ID" + case proxyExtensionBundleID = "PROXY_EXTENSION_BUNDLE_ID" + } + + static var tunnelExtensionBundleID: String { + string(for: .tunnelExtensionBundleID) + } + + static var proxyExtensionBundleID: String { + string(for: .proxyExtensionBundleID) + } + + private static func string(for key: VPNInfoKey) -> String { + guard let bundleID = Bundle.main.object(forInfoDictionaryKey: key.rawValue) as? String else { + fatalError("Info.plist is missing \(key)") + } + + return bundleID + } + +#if !NETWORK_EXTENSION + // for the Main or Launcher Agent app + static func mainAppBundle() -> Bundle { + return Bundle.main + } +#elseif NETP_SYSTEM_EXTENSION + // for the System Extension (Developer ID) + static func mainAppBundle() -> Bundle { + return Bundle(url: .mainAppBundleURL)! + } + // AppEx (App Store) can‘t access Main App Bundle +#endif + + static let keychainType: KeychainType = { +#if NETP_SYSTEM_EXTENSION + .system +#else + .dataProtection(.named(Bundle.main.appGroup(bundle: .netP))) +#endif + }() +} diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift deleted file mode 100644 index e14b7f1e84..0000000000 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// NetworkProtectionBundle.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 -import NetworkProtection - -enum NetworkProtectionBundle { - -#if !NETWORK_EXTENSION - // for the Main or Launcher Agent app - static func mainAppBundle() -> Bundle { - return Bundle.main - } -#elseif NETP_SYSTEM_EXTENSION - // for the System Extension (Developer ID) - static func mainAppBundle() -> Bundle { - return Bundle(url: .mainAppBundleURL)! - } - // AppEx (App Store) can‘t access Main App Bundle -#endif - - static func extensionBundle() -> Bundle { -#if NETWORK_EXTENSION // When this code is compiled for any network-extension - return Bundle.main -#elseif NETP_SYSTEM_EXTENSION // When this code is compiled for the app when configured to use the sysex - let extensionsDirectoryURL = URL(fileURLWithPath: "Contents/Library/SystemExtensions", relativeTo: Bundle.main.bundleURL) - return extensionBundle(at: extensionsDirectoryURL) -#else // When this code is compiled for the app when configured to use the appex - let extensionsDirectoryURL = URL(fileURLWithPath: "Contents/Plugins", relativeTo: Bundle.main.bundleURL) - return extensionBundle(at: extensionsDirectoryURL) -#endif - } - - static func extensionBundle(at url: URL) -> Bundle { - let extensionURLs: [URL] - do { - extensionURLs = try FileManager.default.contentsOfDirectory(at: url, - includingPropertiesForKeys: nil, - options: .skipsHiddenFiles) - } catch let error { - fatalError("🔵 Failed to get the contents of \(url.absoluteString): \(error.localizedDescription)") - } - - // This should be updated to work well with other extensions - guard let extensionURL = extensionURLs.first else { - fatalError("🔵 Failed to find any system extensions") - } - - guard let extensionBundle = Bundle(url: extensionURL) else { - fatalError("🔵 Failed to create a bundle with URL \(extensionURL.absoluteString)") - } - - return extensionBundle - } - - static let keychainType: KeychainType = { -#if NETP_SYSTEM_EXTENSION - .system -#else - .dataProtection(.named(Bundle.main.appGroup(bundle: .netP))) -#endif - }() -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index e1696a7aba..f54236f387 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -22,6 +22,7 @@ import AppKit import Common import Foundation import NetworkProtection +import NetworkProtectionProxy import SwiftUI /// Controller for the Network Protection debug menu. @@ -29,6 +30,10 @@ import SwiftUI @MainActor final class NetworkProtectionDebugMenu: NSMenu { + private let transparentProxySettings = TransparentProxySettings(defaults: .netP) + + // MARK: - Menus + private let environmentMenu = NSMenu() private let preferredServerMenu: NSMenu @@ -39,7 +44,9 @@ final class NetworkProtectionDebugMenu: NSMenu { private let resetToDefaults = NSMenuItem(title: "Reset Settings to defaults", action: #selector(NetworkProtectionDebugMenu.resetSettings)) - private let exclusionsMenu = NSMenu() + private let excludedRoutesMenu = NSMenu() + private let excludeDDGBrowserTrafficFromVPN = NSMenuItem(title: "DDG Browser", action: #selector(toggleExcludeDDGBrowser)) + private let excludeDBPTrafficFromVPN = NSMenuItem(title: "DBP Background Agent", action: #selector(toggleExcludeDBPBackgroundAgent)) private let shouldEnforceRoutesMenuItem = NSMenuItem(title: "Kill Switch (enforceRoutes)", action: #selector(NetworkProtectionDebugMenu.toggleEnforceRoutesAction)) private let shouldIncludeAllNetworksMenuItem = NSMenuItem(title: "includeAllNetworks", action: #selector(NetworkProtectionDebugMenu.toggleIncludeAllNetworks)) @@ -89,7 +96,6 @@ final class NetworkProtectionDebugMenu: NSMenu { .targetting(self) shouldEnforceRoutesMenuItem .targetting(self) - NSMenuItem(title: "Excluded Routes").submenu(exclusionsMenu) NSMenuItem.separator() NSMenuItem(title: "Send Test Notification", action: #selector(NetworkProtectionDebugMenu.sendTestNotification)) @@ -104,6 +110,14 @@ final class NetworkProtectionDebugMenu: NSMenu { NSMenuItem(title: "Environment") .submenu(environmentMenu) + NSMenuItem(title: "Exclusions") { + NSMenuItem(title: "Excluded Apps") { + excludeDDGBrowserTrafficFromVPN.targetting(self) + excludeDBPTrafficFromVPN.targetting(self) + } + NSMenuItem(title: "Excluded Routes").submenu(excludedRoutesMenu) + } + NSMenuItem(title: "Preferred Server").submenu(preferredServerMenu) NSMenuItem(title: "Registration Key") { @@ -172,8 +186,8 @@ final class NetworkProtectionDebugMenu: NSMenu { populateNetworkProtectionServerListMenuItems() populateNetworkProtectionRegistrationKeyValidityMenuItems() - exclusionsMenu.delegate = self - exclusionsMenu.autoenablesItems = false + excludedRoutesMenu.delegate = self + excludedRoutesMenu.autoenablesItems = false populateExclusionsMenuItems() } @@ -391,7 +405,7 @@ final class NetworkProtectionDebugMenu: NSMenu { } private func populateExclusionsMenuItems() { - exclusionsMenu.removeAllItems() + excludedRoutesMenu.removeAllItems() for item in settings.excludedRoutes { let menuItem: NSMenuItem @@ -406,7 +420,7 @@ final class NetworkProtectionDebugMenu: NSMenu { target: self, representedObject: range.stringRepresentation) } - exclusionsMenu.addItem(menuItem) + excludedRoutesMenu.addItem(menuItem) } // Only allow testers to enter a custom code if they're on the waitlist, to simulate the correct path through the flow @@ -419,6 +433,7 @@ final class NetworkProtectionDebugMenu: NSMenu { override func update() { updateEnvironmentMenu() + updateExclusionsMenu() updatePreferredServerMenu() updateRekeyValidityMenu() updateNetworkProtectionMenuItemsState() @@ -588,6 +603,7 @@ final class NetworkProtectionDebugMenu: NSMenu { } // MARK: Environment + @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { let title = menuItem.title let selectedEnvironment: VPNSettings.SelectedEnvironment @@ -608,6 +624,24 @@ final class NetworkProtectionDebugMenu: NSMenu { settings.selectedServer = .automatic } } + + // MARK: - Exclusions + + private let dbpBackgroundAppIdentifier = Bundle.main.dbpBackgroundAgentBundleId + private let ddgBrowserAppIdentifier = Bundle.main.bundleIdentifier! + + private func updateExclusionsMenu() { + excludeDBPTrafficFromVPN.state = transparentProxySettings.isExcluding(dbpBackgroundAppIdentifier) ? .on : .off + excludeDDGBrowserTrafficFromVPN.state = transparentProxySettings.isExcluding(ddgBrowserAppIdentifier) ? .on : .off + } + + @objc private func toggleExcludeDBPBackgroundAgent() { + transparentProxySettings.toggleExclusion(for: dbpBackgroundAppIdentifier) + } + + @objc private func toggleExcludeDDGBrowser() { + transparentProxySettings.toggleExclusion(for: ddgBrowserAppIdentifier) + } } extension NetworkProtectionDebugMenu: NSMenuDelegate { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index f67223a545..0bd4975196 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -24,6 +24,7 @@ import SwiftUI import Common import NetworkExtension import NetworkProtection +import NetworkProtectionProxy import NetworkProtectionUI import Networking import PixelKit @@ -38,6 +39,8 @@ typealias NetworkProtectionConfigChangeHandler = () -> Void final class NetworkProtectionTunnelController: TunnelController, TunnelSessionProvider { + // MARK: - Settings + let settings: VPNSettings // MARK: - Combine Cancellables @@ -60,6 +63,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// private let controllerErrorStore = NetworkProtectionControllerErrorStore() + private let notificationCenter: NotificationCenter + // MARK: - VPN Tunnel & Configuration /// Auth token store @@ -95,6 +100,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Loads the configuration matching our ``extensionID``. /// + @MainActor public var manager: NETunnelProviderManager? { get async { if let internalManager { @@ -139,13 +145,14 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr init(networkExtensionBundleID: String, networkExtensionController: NetworkExtensionController, settings: VPNSettings, - notificationCenter: NotificationCenter = .default, tokenStore: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), + notificationCenter: NotificationCenter = .default, logger: NetworkProtectionLogger = DefaultNetworkProtectionLogger()) { self.logger = logger self.networkExtensionBundleID = networkExtensionBundleID self.networkExtensionController = networkExtensionController + self.notificationCenter = notificationCenter self.settings = settings self.tokenStore = tokenStore @@ -254,7 +261,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr tunnelManager.protocolConfiguration = { let protocolConfiguration = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() protocolConfiguration.serverAddress = "127.0.0.1" // Dummy address... the NetP service will take care of grabbing a real server - protocolConfiguration.providerBundleIdentifier = NetworkProtectionBundle.extensionBundle().bundleIdentifier + protocolConfiguration.providerBundleIdentifier = Bundle.tunnelExtensionBundleID protocolConfiguration.providerConfiguration = [ NetworkProtectionOptionKey.defaultPixelHeaders: APIRequest.Headers().httpHeaders, NetworkProtectionOptionKey.includedRoutes: includedRoutes().map(\.stringRepresentation) as NSArray @@ -304,6 +311,14 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } } + // MARK: - Connection + + public var status: NEVPNStatus { + get async { + await connection?.status ?? .disconnected + } + } + // MARK: - Connection Status Querying /// Queries Network Protection to know if its VPN is connected. diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 5770b78a2f..3a3a392736 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -224,7 +224,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) - let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: NetworkProtectionBundle.keychainType, + let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, serviceName: Self.tokenServiceName, errorEvents: debugEvents) let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings) @@ -232,7 +232,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { super.init(notificationsPresenter: notificationsPresenter, tunnelHealthStore: tunnelHealthStore, controllerErrorStore: controllerErrorStore, - keychainType: NetworkProtectionBundle.keychainType, + keychainType: Bundle.keychainType, tokenStore: tokenStore, debugEvents: debugEvents, providerEvents: Self.packetTunnelProviderEvents, @@ -323,13 +323,6 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { case missingPixelHeaders } - override func prepareToConnect(using provider: NETunnelProviderProtocol?) { - super.prepareToConnect(using: provider) - - guard PixelKit.shared == nil, let options = provider?.providerConfiguration else { return } - try? loadDefaultPixelHeaders(from: options) - } - public override func loadVendorOptions(from provider: NETunnelProviderProtocol?) throws { try super.loadVendorOptions(from: provider) @@ -350,6 +343,15 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { setupPixels(defaultHeaders: defaultPixelHeaders) } + // MARK: - Overrideable Connection Events + + override func prepareToConnect(using provider: NETunnelProviderProtocol?) { + super.prepareToConnect(using: provider) + + guard PixelKit.shared == nil, let options = provider?.providerConfiguration else { return } + try? loadDefaultPixelHeaders(from: options) + } + // MARK: - Start/Stop Tunnel override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift new file mode 100644 index 0000000000..d300309ec6 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift @@ -0,0 +1,94 @@ +// +// MacTransparentProxyProvider.swift +// +// Copyright © 2024 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 Combine +import Common +import Foundation +import Networking +import NetworkExtension +import NetworkProtectionProxy +import os.log // swiftlint:disable:this enforce_os_log_wrapper +import PixelKit + +final class MacTransparentProxyProvider: TransparentProxyProvider { + + static var vpnProxyLogger = Logger(subsystem: OSLog.subsystem, category: "VPN Proxy") + + private var cancellables = Set() + + @objc init() { + let loadSettingsFromStartupOptions: Bool = { +#if NETP_SYSTEM_EXTENSION + true +#else + false +#endif + }() + + let settings: TransparentProxySettings = { +#if NETP_SYSTEM_EXTENSION + /// Because our System Extension is running in the system context and doesn't have access + /// to shared user defaults, we just make it use the `.standard` defaults. + TransparentProxySettings(defaults: .standard) +#else + /// Because our App Extension is running in the user context and has access + /// to shared user defaults, we take advantage of this and use the `.netP` defaults. + TransparentProxySettings(defaults: .netP) +#endif + }() + + let configuration = TransparentProxyProvider.Configuration( + loadSettingsFromProviderConfiguration: loadSettingsFromStartupOptions) + + super.init(settings: settings, + configuration: configuration, + logger: Self.vpnProxyLogger) + + eventHandler = eventHandler(_:) + +#if !NETP_SYSTEM_EXTENSION + let dryRun: Bool +#if DEBUG + dryRun = true +#else + dryRun = false +#endif + + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: "vpnProxyExtension", + defaultHeaders: [:], + log: .networkProtectionPixel, + defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequest.Headers(additionalHeaders: headers) + let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) + let request = APIRequest(configuration: configuration) + + request.fetch { _, error in + onComplete(error == nil, error) + } + } +#endif + } + + private func eventHandler(_ event: TransparentProxyProvider.Event) { + PixelKit.fire(event) + } +} diff --git a/DuckDuckGo/NetworkProtectionAppExtension.entitlements b/DuckDuckGo/NetworkProtectionAppExtension.entitlements index 13dd983ca1..d37610bb07 100644 --- a/DuckDuckGo/NetworkProtectionAppExtension.entitlements +++ b/DuckDuckGo/NetworkProtectionAppExtension.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.security.app-sandbox diff --git a/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift b/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift new file mode 100644 index 0000000000..85891c6604 --- /dev/null +++ b/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift @@ -0,0 +1,64 @@ +// +// BrowserWindowManager.swift +// +// Copyright © 2024 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 AppKit +import Foundation +import WebKit + +/// A class that offers functionality to quickly show an interactive browser window. +/// +/// This class is meant to aid with debugging and should not be included in release builds. +/// . +final class BrowserWindowManager: NSObject { + private var interactiveBrowserWindow: NSWindow? + + @MainActor + func show(domain: String) { + if let interactiveBrowserWindow, interactiveBrowserWindow.isVisible { + return + } + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, defer: false) + window.center() + window.title = "Web Browser" + window.delegate = self + interactiveBrowserWindow = window + + // Create the WKWebView. + let webView = WKWebView(frame: window.contentView!.bounds) + webView.autoresizingMask = [.width, .height] + window.contentView!.addSubview(webView) + + // Load a URL. + let url = URL(string: domain)! + let request = URLRequest(url: url) + webView.load(request) + + // Show the window. + window.makeKeyAndOrderFront(nil) + } +} + +extension BrowserWindowManager: NSWindowDelegate { + func windowWillClose(_ notification: Notification) { + interactiveBrowserWindow = nil + } +} diff --git a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift index 1d7c0403fb..c452200f7b 100644 --- a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift @@ -17,10 +17,10 @@ // import Combine -import Foundation +import Common import DataBrokerProtection +import Foundation import PixelKit -import Common /// Manages the IPC service for the Agent app /// @@ -28,6 +28,7 @@ import Common /// demand interaction with. /// final class IPCServiceManager { + private var browserWindowManager: BrowserWindowManager private let ipcServer: DataBrokerProtectionIPCServer private let scheduler: DataBrokerProtectionScheduler private let pixelHandler: EventMapping @@ -41,6 +42,8 @@ final class IPCServiceManager { self.scheduler = scheduler self.pixelHandler = pixelHandler + browserWindowManager = BrowserWindowManager() + ipcServer.serverDelegate = self ipcServer.activate() } @@ -102,4 +105,10 @@ extension IPCServiceManager: IPCServerInterface { pixelHandler.fire(.ipcServerRunAllOperations) scheduler.runAllOperations(showWebView: showWebView) } + + func openBrowser(domain: String) { + Task { @MainActor in + browserWindowManager.show(domain: domain) + } + } } diff --git a/DuckDuckGoVPN/DuckDuckGoVPN.entitlements b/DuckDuckGoVPN/DuckDuckGoVPN.entitlements index 653311b9ec..2797c3f947 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPN.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPN.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.developer.system-extension.install diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index add4af8a6d..f14bbac165 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -23,7 +23,7 @@ import LoginItems import Networking import NetworkExtension import NetworkProtection -import NetworkProtectionIPC +import NetworkProtectionProxy import NetworkProtectionUI import ServiceManagement import PixelKit @@ -60,18 +60,82 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private var cancellables = Set() - var networkExtensionBundleID: String { - Bundle.main.networkExtensionBundleID + var proxyExtensionBundleID: String { + Bundle.proxyExtensionBundleID } -#if NETWORK_PROTECTION - private lazy var networkExtensionController = NetworkExtensionController(extensionBundleID: networkExtensionBundleID) + var tunnelExtensionBundleID: String { + Bundle.tunnelExtensionBundleID + } + + private lazy var networkExtensionController = NetworkExtensionController(extensionBundleID: tunnelExtensionBundleID) + + private var storeProxySettingsInProviderConfiguration: Bool { +#if NETP_SYSTEM_EXTENSION + true +#else + false #endif + } private lazy var tunnelSettings = VPNSettings(defaults: .netP) + private lazy var proxySettings = TransparentProxySettings(defaults: .netP) + + @MainActor + private lazy var vpnProxyLauncher = VPNProxyLauncher( + tunnelController: tunnelController, + proxyController: proxyController) + + @MainActor + private lazy var proxyController: TransparentProxyController = { + let controller = TransparentProxyController( + extensionID: proxyExtensionBundleID, + storeSettingsInProviderConfiguration: storeProxySettingsInProviderConfiguration, + settings: proxySettings) { [weak self] manager in + guard let self else { return } + + manager.localizedDescription = "DuckDuckGo VPN Proxy" + + if !manager.isEnabled { + manager.isEnabled = true + } + + manager.protocolConfiguration = { + let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() + protocolConfiguration.serverAddress = "127.0.0.1" // Dummy address... the NetP service will take care of grabbing a real server + protocolConfiguration.providerBundleIdentifier = self.proxyExtensionBundleID + + // always-on + protocolConfiguration.disconnectOnSleep = false + + // kill switch + // protocolConfiguration.enforceRoutes = false + + // this setting breaks Connection Tester + // protocolConfiguration.includeAllNetworks = settings.includeAllNetworks + + // This is intentionally not used but left here for documentation purposes. + // The reason for this is that we want to have full control of the routes that + // are excluded, so instead of using this setting we're just configuring the + // excluded routes through our VPNSettings class, which our extension reads directly. + // protocolConfiguration.excludeLocalNetworks = settings.excludeLocalNetworks + return protocolConfiguration + }() + } + + controller.eventHandler = handleControllerEvent(_:) + + return controller + }() + + private func handleControllerEvent(_ event: TransparentProxyController.Event) { + PixelKit.fire(event) + } + + @MainActor private lazy var tunnelController = NetworkProtectionTunnelController( - networkExtensionBundleID: networkExtensionBundleID, + networkExtensionBundleID: tunnelExtensionBundleID, networkExtensionController: networkExtensionController, settings: tunnelSettings) @@ -79,6 +143,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { /// /// This is used by our main app to control the tunnel through the VPN login item. /// + @MainActor private lazy var tunnelControllerIPCService: TunnelControllerIPCService = { let ipcServer = TunnelControllerIPCService( tunnelController: tunnelController, @@ -88,17 +153,19 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { return ipcServer }() + @MainActor + private lazy var statusObserver = ConnectionStatusObserverThroughSession( + tunnelSessionProvider: tunnelController, + platformNotificationCenter: NSWorkspace.shared.notificationCenter, + platformDidWakeNotification: NSWorkspace.didWakeNotification) + + @MainActor private lazy var statusReporter: NetworkProtectionStatusReporter = { let errorObserver = ConnectionErrorObserverThroughSession( tunnelSessionProvider: tunnelController, platformNotificationCenter: NSWorkspace.shared.notificationCenter, platformDidWakeNotification: NSWorkspace.didWakeNotification) - let statusObserver = ConnectionStatusObserverThroughSession( - tunnelSessionProvider: tunnelController, - platformNotificationCenter: NSWorkspace.shared.notificationCenter, - platformDidWakeNotification: NSWorkspace.didWakeNotification) - let serverInfoObserver = ConnectionServerInfoObserverThroughSession( tunnelSessionProvider: tunnelController, platformNotificationCenter: NSWorkspace.shared.notificationCenter, @@ -113,6 +180,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { ) }() + @MainActor private lazy var vpnAppEventsHandler = { VPNAppEventsHandler(tunnelController: tunnelController) }() @@ -175,8 +243,9 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { bouncer.requireAuthTokenOrKillApp() - // Initialize the IPC server + // Initialize lazy properties _ = tunnelControllerIPCService + _ = vpnProxyLauncher let dryRun: Bool diff --git a/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements b/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements index a6ed34f64f..f531d0bc0c 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.developer.system-extension.install diff --git a/DuckDuckGoVPN/Info-AppStore.plist b/DuckDuckGoVPN/Info-AppStore.plist index 7627fdd9c9..a1b2b02a02 100644 --- a/DuckDuckGoVPN/Info-AppStore.plist +++ b/DuckDuckGoVPN/Info-AppStore.plist @@ -6,8 +6,10 @@ $(DISTRIBUTED_NOTIFICATIONS_PREFIX) NETP_APP_GROUP $(NETP_APP_GROUP) - SYSEX_BUNDLE_ID - $(SYSEX_BUNDLE_ID) + PROXY_EXTENSION_BUNDLE_ID + $(PROXY_EXTENSION_BUNDLE_ID) + TUNNEL_EXTENSION_BUNDLE_ID + $(TUNNEL_EXTENSION_BUNDLE_ID) LSApplicationCategoryType public.app-category.productivity CFBundleShortVersionString diff --git a/DuckDuckGoVPN/Info.plist b/DuckDuckGoVPN/Info.plist index 7627fdd9c9..a1b2b02a02 100644 --- a/DuckDuckGoVPN/Info.plist +++ b/DuckDuckGoVPN/Info.plist @@ -6,8 +6,10 @@ $(DISTRIBUTED_NOTIFICATIONS_PREFIX) NETP_APP_GROUP $(NETP_APP_GROUP) - SYSEX_BUNDLE_ID - $(SYSEX_BUNDLE_ID) + PROXY_EXTENSION_BUNDLE_ID + $(PROXY_EXTENSION_BUNDLE_ID) + TUNNEL_EXTENSION_BUNDLE_ID + $(TUNNEL_EXTENSION_BUNDLE_ID) LSApplicationCategoryType public.app-category.productivity CFBundleShortVersionString diff --git a/DuckDuckGoVPN/VPNProxyLauncher.swift b/DuckDuckGoVPN/VPNProxyLauncher.swift new file mode 100644 index 0000000000..c99d187cf2 --- /dev/null +++ b/DuckDuckGoVPN/VPNProxyLauncher.swift @@ -0,0 +1,149 @@ +// +// VPNProxyLauncher.swift +// +// Copyright © 2024 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 Combine +import Foundation +import NetworkProtectionProxy +import NetworkExtension + +/// Starts and stops the VPN proxy component. +/// +/// This class looks at the tunnel and the proxy components and their status and settings, and decides based on +/// a number of conditions whether to start the proxy, stop it, or just leave it be. +/// +@MainActor +final class VPNProxyLauncher { + private let tunnelController: NetworkProtectionTunnelController + private let proxyController: TransparentProxyController + private let notificationCenter: NotificationCenter + private var cancellables = Set() + + init(tunnelController: NetworkProtectionTunnelController, + proxyController: TransparentProxyController, + notificationCenter: NotificationCenter = .default) { + + self.notificationCenter = notificationCenter + self.proxyController = proxyController + self.tunnelController = tunnelController + + subscribeToStatusChanges() + subscribeToProxySettingChanges() + } + + // MARK: - Status Changes + + private func subscribeToStatusChanges() { + notificationCenter.publisher(for: .NEVPNStatusDidChange) + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink(receiveValue: statusChanged(notification:)) + .store(in: &cancellables) + } + + private func statusChanged(notification: Notification) { + Task { @MainActor in + let isProxyConnectionStatusChange = await proxyController.connection == notification.object as? NEVPNConnection + + try await startOrStopProxyIfNeeded(isProxyConnectionStatusChange: isProxyConnectionStatusChange) + } + } + + // MARK: - Proxy Settings Changes + + private func subscribeToProxySettingChanges() { + proxyController.settings.changePublisher + .sink(receiveValue: proxySettingChanged(_:)) + .store(in: &cancellables) + } + + private func proxySettingChanged(_ change: TransparentProxySettings.Change) { + Task { @MainActor in + try await startOrStopProxyIfNeeded() + } + } + + // MARK: - Auto starting & stopping the proxy component + + private var isControllingProxy = false + + private func startOrStopProxyIfNeeded(isProxyConnectionStatusChange: Bool = false) async throws { + if await shouldStartProxy { + guard !isControllingProxy else { + return + } + + isControllingProxy = true + + // When we're auto-starting the proxy because its own status changed to + // disconnected, we want to give it a pause because if it fails to connect again + // we risk the proxy entering a frenetic connect / disconnect loop + if isProxyConnectionStatusChange { + // If the proxy connection was stopped, let's wait a bit before trying to enable it again + try await Task.sleep(interval: .seconds(10)) + + // And we want to check again if the proxy still needs to start after waiting + guard await shouldStartProxy else { + return + } + } + + do { + try await proxyController.start() + isControllingProxy = false + } catch { + isControllingProxy = false + throw error + } + } else if await shouldStopProxy { + guard !isControllingProxy else { + return + } + + isControllingProxy = true + await proxyController.stop() + isControllingProxy = false + } + } + + private var shouldStartProxy: Bool { + get async { + let proxyIsDisconnected = await proxyController.status == .disconnected + let tunnelIsConnected = await tunnelController.status == .connected + + // Starting the proxy only when it's required for active features + // is a product decision. It may change once we decide the proxy + // is stable enough to be running at all times. + return proxyIsDisconnected + && tunnelIsConnected + && proxyController.isRequiredForActiveFeatures + } + } + + private var shouldStopProxy: Bool { + get async { + let proxyIsConnected = await proxyController.status == .connected + let tunnelIsDisconnected = await tunnelController.status == .disconnected + + // Stopping the proxy when it's not required for active features + // is a product decision. It may change once we decide the proxy + // is stable enough to be running at all times. + return proxyIsConnected + && (tunnelIsDisconnected || !proxyController.isRequiredForActiveFeatures) + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index fb62529ee7..64fa721012 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: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 4a6f58ac28..2aa3953437 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -17,9 +17,9 @@ // import Combine +import Common import Foundation import XPCHelper -import Common /// This protocol describes the server-side IPC interface for controlling the tunnel /// @@ -150,6 +150,17 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { // If you add a completion block, please remember to call it here too! }) } + + public func openBrowser(domain: String) { + self.pixelHandler.fire(.ipcServerRunAllOperations) + xpc.execute(call: { server in + server.openBrowser(domain: domain) + }, xpcReplyErrorHandler: { error in + os_log("Error \(error.localizedDescription)") + // Intentional no-op as there's no completion block + // If you add a completion block, please remember to call it here too! + }) + } } // MARK: - Incoming communication from the server diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index fde0274a5f..a2bc3d0e56 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -42,6 +42,12 @@ public protocol IPCServerInterface: AnyObject { func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runAllOperations(showWebView: Bool) + + // MARK: - Debugging Features + + /// Opens a browser window with the specified domain + /// + func openBrowser(domain: String) } /// This protocol describes the server-side XPC interface. @@ -71,6 +77,12 @@ protocol XPCServerInterface { func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runAllOperations(showWebView: Bool) + + // MARK: - Debugging Features + + /// Opens a browser window with the specified domain + /// + func openBrowser(domain: String) } public final class DataBrokerProtectionIPCServer { @@ -146,4 +158,8 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { func runAllOperations(showWebView: Bool) { serverDelegate?.runAllOperations(showWebView: showWebView) } + + func openBrowser(domain: String) { + serverDelegate?.openBrowser(domain: domain) + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index ad3dfda484..9f12bbab0c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -279,7 +279,6 @@ public enum DataBrokerProtectionPixels { } extension DataBrokerProtectionPixels: PixelKitEvent { - public var name: String { switch self { case .parentChildMatches: return "m_mac_dbp_macos_parent-child-broker-matches" diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index bbe7e894b8..a631a199d9 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -13,7 +13,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Package.resolved b/LocalPackages/NetworkProtectionMac/Package.resolved new file mode 100644 index 0000000000..08c5add0f4 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Package.resolved @@ -0,0 +1,104 @@ +{ + "pins" : [ + { + "identity" : "bloom_cpp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/bloom_cpp.git", + "state" : { + "revision" : "8076199456290b61b4544bf2f4caf296759906a0", + "version" : "3.0.0" + } + }, + { + "identity" : "browserserviceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/BrowserServicesKit", + "state" : { + "revision" : "1f7932fe67a0d8b1ae97e62cb333639353d4772f", + "version" : "101.2.2" + } + }, + { + "identity" : "content-scope-scripts", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/content-scope-scripts", + "state" : { + "revision" : "0b68b0d404d8d4f32296cd84fa160b18b0aeaf44", + "version" : "4.59.1" + } + }, + { + "identity" : "duckduckgo-autofill", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", + "state" : { + "revision" : "b972bc0ab6ee1d57a0a18a197dcc31e40ae6ac57", + "version" : "10.0.3" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/GRDB.swift.git", + "state" : { + "revision" : "9f049d7b97b1e68ffd86744b500660d34a9e79b8", + "version" : "2.3.0" + } + }, + { + "identity" : "privacy-dashboard", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/privacy-dashboard", + "state" : { + "revision" : "38336a574e13090764ba09a6b877d15ee514e371", + "version" : "3.1.1" + } + }, + { + "identity" : "punycodeswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gumob/PunycodeSwift.git", + "state" : { + "revision" : "4356ec54e073741449640d3d50a1fd24fd1e1b8b", + "version" : "2.1.0" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version" : "1.3.0" + } + }, + { + "identity" : "sync_crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/sync_crypto", + "state" : { + "revision" : "2ab6ab6f0f96b259c14c2de3fc948935fc16ac78", + "version" : "0.2.0" + } + }, + { + "identity" : "trackerradarkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "state" : { + "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", + "version" : "1.2.2" + } + }, + { + "identity" : "wireguard-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/wireguard-apple", + "state" : { + "revision" : "2d8172c11478ab11b0f5ad49bdb4f93f4b3d5e0d", + "version" : "1.1.1" + } + } + ], + "version" : 2 +} diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index a1edbe7388..e2dc908671 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -27,10 +27,11 @@ let package = Package( ], products: [ .library(name: "NetworkProtectionIPC", targets: ["NetworkProtectionIPC"]), + .library(name: "NetworkProtectionProxy", targets: ["NetworkProtectionProxy"]), .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems") @@ -50,6 +51,19 @@ let package = Package( plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] ), + // MARK: - NetworkProtectionProxy + + .target( + name: "NetworkProtectionProxy", + dependencies: [ + .product(name: "NetworkProtection", package: "BrowserServicesKit") + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] + ), + // MARK: - NetworkProtectionUI .target( diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift new file mode 100644 index 0000000000..882eb19734 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift @@ -0,0 +1,242 @@ +// +// TCPFlowManager.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 +import NetworkExtension +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// A private global actor to handle UDP flows management +/// +@globalActor +struct TCPFlowActor { + actor ActorType { } + + static let shared: ActorType = ActorType() +} + +@TCPFlowActor +enum RemoteConnectionError: Error { + case complete + case cancelled + case couldNotEstablishConnection(_ error: Error) + case unhandledError(_ error: Error) +} + +final class TCPFlowManager { + private let flow: NEAppProxyTCPFlow + private var connectionTask: Task? + private var connection: NWConnection? + + init(flow: NEAppProxyTCPFlow) { + self.flow = flow + } + + deinit { + // Just making extra sure we don't have any unexpected retain cycle + connection?.stateUpdateHandler = nil + connection?.cancel() + } + + func start(interface: NWInterface) async throws { + guard let remoteEndpoint = flow.remoteEndpoint as? NWHostEndpoint else { + return + } + + try await connectAndStartRunLoop(remoteEndpoint: remoteEndpoint, interface: interface) + } + + private func connectAndStartRunLoop(remoteEndpoint: NWHostEndpoint, interface: NWInterface) async throws { + let remoteConnection = try await connect(to: remoteEndpoint, interface: interface) + try await flow.open(withLocalEndpoint: nil) + + do { + try await startDataCopyLoop(for: remoteConnection) + + remoteConnection.cancel() + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + } catch { + remoteConnection.cancel() + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + } + + func connect(to remoteEndpoint: NWHostEndpoint, interface: NWInterface) async throws -> NWConnection { + try await withCheckedThrowingContinuation { continuation in + connect(to: remoteEndpoint, interface: interface) { result in + switch result { + case .success(let connection): + continuation.resume(returning: connection) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + func connect(to remoteEndpoint: NWHostEndpoint, interface: NWInterface, completion: @escaping @TCPFlowActor (Result) -> Void) { + let host = Network.NWEndpoint.Host(remoteEndpoint.hostname) + let port = Network.NWEndpoint.Port(remoteEndpoint.port)! + + let parameters = NWParameters.tcp + parameters.preferNoProxies = true + parameters.requiredInterface = interface + parameters.prohibitedInterfaceTypes = [.other] + + let connection = NWConnection(host: host, port: port, using: parameters) + self.connection = connection + + connection.stateUpdateHandler = { (state: NWConnection.State) in + Task { @TCPFlowActor in + switch state { + case .ready: + connection.stateUpdateHandler = nil + completion(.success(connection)) + case .cancelled: + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.cancelled)) + case .failed(let error), .waiting(let error): + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.couldNotEstablishConnection(error))) + default: + break + } + } + } + + connection.start(queue: .global()) + } + + private func startDataCopyLoop(for remoteConnection: NWConnection) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { [weak self] in + while true { + guard let self else { + throw RemoteConnectionError.cancelled + } + + try Task.checkCancellation() + try await self.copyOutboundTraffic(to: remoteConnection) + } + } + + group.addTask { [weak self] in + while true { + guard let self else { + throw RemoteConnectionError.cancelled + } + + try Task.checkCancellation() + try await self.copyInboundTraffic(from: remoteConnection) + } + } + + while !group.isEmpty { + do { + try await group.next() + + } catch { + group.cancelAll() + throw error + } + } + } + } + + @MainActor + func closeFlow(remoteConnection: NWConnection, error: Error?) { + remoteConnection.forceCancel() + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + + static let maxReceiveSize: Int = Int(Measurement(value: 2, unit: UnitInformationStorage.megabytes).converted(to: .bytes).value) + + func copyInboundTraffic(from remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @TCPFlowActor in + remoteConnection.receive(minimumIncompleteLength: 1, + maximumLength: Self.maxReceiveSize) { [weak flow] (data, _, isComplete, error) in + guard let flow else { + continuation.resume(throwing: RemoteConnectionError.cancelled) + return + } + + switch (data, isComplete, error) { + case (.some(let data), _, _) where !data.isEmpty: + flow.write(data) { writeError in + if let writeError { + continuation.resume(throwing: writeError) + remoteConnection.cancel() + } else { + continuation.resume() + } + } + case (_, isComplete, _) where isComplete == true: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + case (_, _, .some(let error)): + continuation.resume(throwing: RemoteConnectionError.unhandledError(error)) + remoteConnection.cancel() + default: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + } + } + } + } + } + + func copyOutboundTraffic(to remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @TCPFlowActor in + flow.readData { data, error in + switch (data, error) { + case (.some(let data), _) where !data.isEmpty: + remoteConnection.send(content: data, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + remoteConnection.cancel() + return + } + + continuation.resume() + })) + case (_, .some(let error)): + continuation.resume(throwing: error) + remoteConnection.cancel() + default: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + } + } + } + } + } +} + +extension TCPFlowManager: Hashable { + static func == (lhs: TCPFlowManager, rhs: TCPFlowManager) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(flow) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift new file mode 100644 index 0000000000..000f37d20e --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift @@ -0,0 +1,329 @@ +// +// UDPFlowManager.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 +import NetworkExtension +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// A private global actor to handle UDP flows management +/// +@globalActor +struct UDPFlowActor { + actor ActorType { } + + static let shared: ActorType = ActorType() +} + +/// Class to handle UDP connections +/// +/// This is necessary because as described in the reference comment for this implementation (see ``UDPFlowManager``'s documentation) +/// it's noted that a single UDP flow can have to manage multiple connections. +/// +@UDPFlowActor +final class UDPConnectionManager { + let endpoint: NWEndpoint + private let connection: NWConnection + private let onReceive: (_ endpoint: NWEndpoint, _ result: Result) async -> Void + + init(endpoint: NWHostEndpoint, interface: NWInterface?, onReceive: @UDPFlowActor @escaping (_ endpoint: NWEndpoint, _ result: Result) async -> Void) { + let host = Network.NWEndpoint.Host(endpoint.hostname) + let port = Network.NWEndpoint.Port(endpoint.port)! + + let parameters = NWParameters.udp + parameters.preferNoProxies = true + parameters.requiredInterface = interface + parameters.prohibitedInterfaceTypes = [.other] + + let connection = NWConnection(host: host, port: port, using: parameters) + + self.connection = connection + self.endpoint = endpoint + self.onReceive = onReceive + } + + deinit { + // Just making extra sure we don't retain anything we don't need to + connection.stateUpdateHandler = nil + connection.cancel() + } + + // MARK: - General Operation + + /// Starts the operation of this connection manager + /// + /// Can be called multiple times safely. + /// + private func start() async throws { + guard connection.state == .setup else { + return + } + + try await connect() + + Task { + while true { + do { + let datagram = try await receive() + await onReceive(endpoint, .success(datagram)) + } catch { + connection.cancel() + await onReceive(endpoint, .failure(error)) + break + } + } + } + } + + // MARK: - Connection Management + + private func connect() async throws { + try await withCheckedThrowingContinuation { continuation in + connect { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + private func connect(completion: @escaping (Result) -> Void) { + connection.stateUpdateHandler = { [connection] (state: NWConnection.State) in + switch state { + case .ready: + connection.stateUpdateHandler = nil + completion(.success(())) + case .cancelled: + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.cancelled)) + case .failed(let error), .waiting(let error): + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.couldNotEstablishConnection(error))) + default: + break + } + } + + connection.start(queue: .global()) + } + + // MARK: - Receiving from remote + + private func receive() async throws -> Data { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.receiveMessage { [weak self] data, _, isComplete, error in + + guard self != nil else { + continuation.resume(throwing: RemoteConnectionError.cancelled) + return + } + + switch (data, isComplete, error) { + case (let data?, _, _): + continuation.resume(returning: data) + case (_, true, _): + continuation.resume(throwing: RemoteConnectionError.cancelled) + case (_, _, let error?): + continuation.resume(throwing: RemoteConnectionError.unhandledError(error)) + default: + continuation.resume(throwing: RemoteConnectionError.cancelled) + } + } + } + } + + // MARK: - Writing datagrams + + func write(datagram: Data) async throws { + try await start() + + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.send(content: datagram, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + })) + } + } +} + +extension UDPConnectionManager: Hashable, Equatable { + // MARK: - Equatable + + static func == (lhs: UDPConnectionManager, rhs: UDPConnectionManager) -> Bool { + lhs.endpoint == rhs.endpoint + } + + // MARK: - Hashable + + func hash(into hasher: inout Hasher) { + hasher.combine(endpoint) + } +} + +/// UDP flow manager class +/// +/// There is documentation explaining how to handle TCP flows here: +/// https://developer.apple.com/documentation/networkextension/app_proxy_provider/handling_flow_copying?changes=_8 +/// +/// Unfortunately there isn't good official documentation showcasing how to implement UDP flow management. +/// The best we could fine are two comments by an Apple engineer that shine some light on how that implementation should be like: +/// https://developer.apple.com/forums/thread/678464?answerId=671531022#671531022 +/// https://developer.apple.com/forums/thread/678464?answerId=671892022#671892022 +/// +/// This class is the result of implementing the description found in that comment. +/// +@UDPFlowActor +final class UDPFlowManager { + private let flow: NEAppProxyUDPFlow + private var interface: NWInterface? + + private var connectionManagers = [NWEndpoint: UDPConnectionManager]() + + init(flow: NEAppProxyUDPFlow) { + self.flow = flow + } + + func start(interface: NWInterface) async throws { + self.interface = interface + try await connectAndStartRunLoop() + } + + private func connectAndStartRunLoop() async throws { + do { + try await flow.open(withLocalEndpoint: nil) + try await startDataCopyLoop() + + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + } catch { + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + } + + private func startDataCopyLoop() async throws { + while true { + try await copyOutoundTraffic() + } + } + + func copyInboundTraffic(endpoint: NWEndpoint, result: Result) async { + switch result { + case .success(let data): + do { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + flow.writeDatagrams([data], sentBy: [endpoint]) { error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + } + } + } catch { + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + case .failure: + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + } + + func copyOutoundTraffic() async throws { + let (datagrams, endpoints) = try await read() + + // Ref: https://developer.apple.com/documentation/networkextension/neappproxyudpflow/1406576-readdatagrams + if datagrams.isEmpty || endpoints.isEmpty { + throw NEAppProxyFlowError(.aborted) + } + + for (datagram, endpoint) in zip(datagrams, endpoints) { + guard let endpoint = endpoint as? NWHostEndpoint else { + // Not sure what to do about this... + continue + } + + let manager = connectionManagers[endpoint] ?? { + let manager = UDPConnectionManager(endpoint: endpoint, interface: interface, onReceive: copyInboundTraffic) + connectionManagers[endpoint] = manager + return manager + }() + + do { + try await manager.write(datagram: datagram) + } catch { + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + } + } + + /// Reads datagrams from the flow. + /// + /// Apple's documentation is very bad here, but it seems each datagram is corresponded with an endpoint at the same position in the array + /// as mentioned here: https://developer.apple.com/forums/thread/75893 + /// + private func read() async throws -> (datagrams: [Data], endpoints: [NWEndpoint]) { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<([Data], [NWEndpoint]), Error>) in + flow.readDatagrams { datagrams, endpoints, error in + if let error { + continuation.resume(throwing: error) + return + } + + guard let datagrams, let endpoints else { + continuation.resume(throwing: NEAppProxyFlowError(.aborted)) + return + } + + continuation.resume(returning: (datagrams, endpoints)) + } + } + } + + private func send(datagram: Data, through remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + remoteConnection.send(content: datagram, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + })) + } + } +} + +extension UDPFlowManager: Hashable { + static func == (lhs: UDPFlowManager, rhs: UDPFlowManager) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(flow) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift new file mode 100644 index 0000000000..12339a673d --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift @@ -0,0 +1,82 @@ +// +// TransparentProxyAppMessageHandler.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// Handles app messages +/// +final class TransparentProxyAppMessageHandler { + + private let settings: TransparentProxySettings + + init(settings: TransparentProxySettings) { + self.settings = settings + } + + func handle(_ data: Data) async -> Data? { + do { + let message = try JSONDecoder().decode(TransparentProxyMessage.self, from: data) + return await handle(message) + } catch { + return nil + } + } + + /// Handles a message. + /// + /// This method will wrap the message into a request with a completion handler, and will process it. + /// The reason why this method wraps the message in a request is to ensure that the response + /// type stays syncrhonized between app and provider. + /// + private func handle(_ message: TransparentProxyMessage) async -> Data? { + await withCheckedContinuation { continuation in + var request: TransparentProxyRequest + + switch message { + case .changeSetting(let change): + request = .changeSetting(change, responseHandler: { + continuation.resume(returning: nil) + }) + } + + handle(request) + } + } + + /// Handles a request and calls the response handler when done. + /// + private func handle(_ request: TransparentProxyRequest) { + switch request { + case .changeSetting(let change, let responseHandler): + handle(change) + responseHandler() + } + } + + /// Handles a settings change. + /// + private func handle(_ settingChange: TransparentProxySettings.Change) { + switch settingChange { + case .appRoutingRules(let routingRules): + settings.appRoutingRules = routingRules + case .excludedDomains(let excludedDomains): + settings.excludedDomains = excludedDomains + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift new file mode 100644 index 0000000000..0881dba3b1 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift @@ -0,0 +1,67 @@ +// +// TransparentProxyRequest.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkExtension + +public enum TransparentProxyMessage: Codable { + case changeSetting(_ change: TransparentProxySettings.Change) +} + +/// A request for the TransparentProxyProvider. +/// +/// This enum associates a request with a response handler making XPC communication simpler. +/// Once the request completes, `responseHandler` will be called with the result. +/// +public enum TransparentProxyRequest { + case changeSetting(_ settingChange: TransparentProxySettings.Change, responseHandler: () -> Void) + + var message: TransparentProxyMessage { + switch self { + case .changeSetting(let change, _): + return .changeSetting(change) + } + } + + func handleResponse(data: Data?) { + switch self { + case .changeSetting(_, let handleResponse): + handleResponse() + } + } +} + +/// Respresents a transparent proxy session. +/// +/// Offers basic IPC communication support for the app that owns the proxy. This mechanism +/// is implemented through `NETunnelProviderSession` which means only the app that +/// owns the proxy can use this class. +/// +public class TransparentProxySession { + + private let session: NETunnelProviderSession + + init(_ session: NETunnelProviderSession) { + self.session = session + } + + func send(_ request: TransparentProxyRequest) throws { + let payload = try JSONEncoder().encode(request.message) + try session.sendProviderMessage(payload, responseHandler: request.handleResponse(data:)) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift new file mode 100644 index 0000000000..b4c9b8cebb --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift @@ -0,0 +1,89 @@ +// +// TransparentProxyControllerPixel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import PixelKit + +extension TransparentProxyController.StartError: PixelKitEventErrorDetails { + public var underlyingError: Error? { + switch self { + case .failedToLoadConfiguration(let underlyingError), + .failedToSaveConfiguration(let underlyingError), + .failedToStartProvider(let underlyingError): + return underlyingError + default: + return nil + } + } +} + +extension TransparentProxyController { + + public enum Event: PixelKitEventV2 { + case startInitiated + case startSuccess + case startFailure(_ error: Error) + + // MARK: - PixelKit.Event + + public var name: String { + namePrefix + "_" + nameSuffix + } + + public var parameters: [String: String]? { + switch self { + case .startInitiated: + return nil + case .startSuccess: + return nil + case .startFailure: + return nil + } + } + + // MARK: - PixelKit Support + + private static let pixelNamePrefix = "vpn_proxy_controller" + + private var namePrefix: String { + Self.pixelNamePrefix + } + + private var nameSuffix: String { + switch self { + case .startInitiated: + return "start_initiated" + case .startFailure: + return "start_failure" + case .startSuccess: + return "start_success" + } + } + + public var error: Error? { + switch self { + case .startInitiated: + return nil + case .startFailure(let error): + return error + case .startSuccess: + return nil + } + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift new file mode 100644 index 0000000000..aff7421bea --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift @@ -0,0 +1,93 @@ +// +// TransparentProxyProviderPixel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import PixelKit + +extension TransparentProxyProvider.StartError: ErrorWithPixelParameters { + public var errorParameters: [String: String] { + switch self { + case .failedToUpdateNetworkSettings(let underlyingError): + return [ + PixelKit.Parameters.underlyingErrorCode: "\((underlyingError as NSError).code)", + PixelKit.Parameters.underlyingErrorDesc: (underlyingError as NSError).domain, + ] + default: + return [:] + } + } +} + +extension TransparentProxyProvider { + + public enum Event: PixelKitEventV2 { + case failedToUpdateNetworkSettings(_ error: Error) + case startInitiated + case startSuccess + case startFailure(_ error: Error) + + private static let pixelNamePrefix = "vpn_proxy_provider" + + private var namePrefix: String { + Self.pixelNamePrefix + } + + private var namePostfix: String { + switch self { + case .failedToUpdateNetworkSettings: + return "failed_to_update_network_settings" + case .startFailure: + return "start_failure" + case .startInitiated: + return "start_initiated" + case .startSuccess: + return "start_success" + } + } + + public var name: String { + namePrefix + "_" + namePostfix + } + + public var parameters: [String: String]? { + switch self { + case.failedToUpdateNetworkSettings: + return nil + case .startFailure: + return nil + case .startInitiated: + return nil + case .startSuccess: + return nil + } + } + + public var error: Error? { + switch self { + case .failedToUpdateNetworkSettings(let error): + return error + case .startInitiated: + return nil + case .startFailure(let error): + return error + case .startSuccess: + return nil + } + } + } +} diff --git a/DuckDuckGoVPN/Bundle+Configuration.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift similarity index 56% rename from DuckDuckGoVPN/Bundle+Configuration.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift index 936c44a4a8..15d4a4e1a1 100644 --- a/DuckDuckGoVPN/Bundle+Configuration.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift @@ -1,7 +1,7 @@ // -// Bundle+Configuration.swift +// VPNAppRoutingRules.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// Copyright © 2024 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. @@ -18,14 +18,4 @@ import Foundation -extension Bundle { - private static let networkExtensionBundleIDKey = "SYSEX_BUNDLE_ID" - - var networkExtensionBundleID: String { - guard let bundleID = object(forInfoDictionaryKey: Self.networkExtensionBundleIDKey) as? String else { - fatalError("Info.plist is missing \(Self.networkExtensionBundleIDKey)") - } - - return bundleID - } -} +public typealias VPNAppRoutingRules = [String: VPNRoutingRule] diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift similarity index 59% rename from DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift index e90627cbc2..b96a0773cf 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift @@ -1,7 +1,7 @@ // -// NetworkProtectionExtensionMachService.swift +// VPNRoutingRule.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// Copyright © 2024 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. @@ -18,14 +18,12 @@ import Foundation -/// Helper methods associated with mach services. +/// Routing rules /// -final class NetworkProtectionExtensionMachService { - - /// Retrieves the mach service name from a network extension bundle. - /// - static func serviceName() -> String { - NetworkProtectionBundle.extensionBundle().machServiceName - } - +/// Note that there's no need for an `ignore` case because that's achieved by not having a rule +/// in the first place. +/// +public enum VPNRoutingRule: Codable, Equatable { + case block + case exclude } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift new file mode 100644 index 0000000000..db010ec2b5 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift @@ -0,0 +1,134 @@ +// +// TransparentProxySettings.swift +// +// Copyright © 2024 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 Combine +import Foundation + +public final class TransparentProxySettings { + public enum Change: Codable { + case appRoutingRules(_ routingRules: VPNAppRoutingRules) + case excludedDomains(_ excludedDomains: [String]) + } + + let defaults: UserDefaults + + private(set) public lazy var changePublisher: AnyPublisher = { + Publishers.MergeMany( + defaults.vpnProxyAppRoutingRulesPublisher + .dropFirst() + .removeDuplicates() + .map { routingRules in + Change.appRoutingRules(routingRules) + }.eraseToAnyPublisher(), + defaults.vpnProxyExcludedDomainsPublisher + .dropFirst() + .removeDuplicates() + .map { excludedDomains in + Change.excludedDomains(excludedDomains) + }.eraseToAnyPublisher() + ).eraseToAnyPublisher() + }() + + public init(defaults: UserDefaults) { + self.defaults = defaults + } + + // MARK: - Settings + + public var appRoutingRules: VPNAppRoutingRules { + get { + defaults.vpnProxyAppRoutingRules + } + + set { + defaults.vpnProxyAppRoutingRules = newValue + } + } + + public var excludedDomains: [String] { + get { + defaults.vpnProxyExcludedDomains + } + + set { + defaults.vpnProxyExcludedDomains = newValue + } + } + + // MARK: - Reset to factory defaults + + public func resetAll() { + defaults.resetVPNProxyAppRoutingRules() + defaults.resetVPNProxyExcludedDomains() + } + + // MARK: - App routing rules logic + + public func isBlocking(_ appIdentifier: String) -> Bool { + appRoutingRules[appIdentifier] == .block + } + + public func isExcluding(_ appIdentifier: String) -> Bool { + appRoutingRules[appIdentifier] == .exclude + } + + public func toggleBlocking(for appIdentifier: String) { + if isBlocking(appIdentifier) { + appRoutingRules.removeValue(forKey: appIdentifier) + } else { + appRoutingRules[appIdentifier] = .block + } + } + + public func toggleExclusion(for appIdentifier: String) { + if isExcluding(appIdentifier) { + appRoutingRules.removeValue(forKey: appIdentifier) + } else { + appRoutingRules[appIdentifier] = .exclude + } + } + + // MARK: - Snapshot support + + public func snapshot() -> TransparentProxySettingsSnapshot { + .init(appRoutingRules: appRoutingRules, excludedDomains: excludedDomains) + } + + public func apply(_ snapshot: TransparentProxySettingsSnapshot) { + appRoutingRules = snapshot.appRoutingRules + excludedDomains = snapshot.excludedDomains + } +} + +extension TransparentProxySettings: CustomStringConvertible { + public var description: String { + """ + TransparentProxySettings {\n + appRoutingRules: \(appRoutingRules)\n + excludedDomains: \(excludedDomains)\n + } + """ + } +} + +public struct TransparentProxySettingsSnapshot: Codable { + public static let key = "com.duckduckgo.TransparentProxySettingsSnapshot" + + public let appRoutingRules: VPNAppRoutingRules + public let excludedDomains: [String] +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift new file mode 100644 index 0000000000..1090ed1626 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift @@ -0,0 +1,79 @@ +// +// UserDefaults+excludedApps.swift +// +// Copyright © 2024 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 Combine +import Foundation + +extension UserDefaults { + private var vpnProxyAppRoutingRulesDataKey: String { + "vpnProxyAppRoutingRulesData" + } + + @objc + dynamic var vpnProxyAppRoutingRulesData: Data? { + get { + object(forKey: vpnProxyAppRoutingRulesDataKey) as? Data + } + + set { + guard let newValue, + newValue.count > 0 else { + + removeObject(forKey: vpnProxyAppRoutingRulesDataKey) + return + } + + set(newValue, forKey: vpnProxyAppRoutingRulesDataKey) + } + } + + var vpnProxyAppRoutingRules: VPNAppRoutingRules { + get { + guard let data = vpnProxyAppRoutingRulesData, + let routingRules = try? JSONDecoder().decode(VPNAppRoutingRules.self, from: data) else { + return [:] + } + + return routingRules + } + + set { + if newValue.isEmpty { + vpnProxyAppRoutingRulesData = nil + return + } + + guard let data = try? JSONEncoder().encode(newValue) else { + vpnProxyAppRoutingRulesData = nil + return + } + + vpnProxyAppRoutingRulesData = data + } + } + + var vpnProxyAppRoutingRulesPublisher: AnyPublisher { + publisher(for: \.vpnProxyAppRoutingRulesData).map { [weak self] _ in + self?.vpnProxyAppRoutingRules ?? [:] + }.eraseToAnyPublisher() + } + + func resetVPNProxyAppRoutingRules() { + removeObject(forKey: vpnProxyAppRoutingRulesDataKey) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift new file mode 100644 index 0000000000..7500178da7 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift @@ -0,0 +1,51 @@ +// +// UserDefaults+excludedDomains.swift +// +// Copyright © 2024 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 Combine +import Foundation + +extension UserDefaults { + private var vpnProxyExcludedDomainsKey: String { + "vpnProxyExcludedDomains" + } + + @objc + dynamic var vpnProxyExcludedDomains: [String] { + get { + object(forKey: vpnProxyExcludedDomainsKey) as? [String] ?? [] + } + + set { + guard newValue.count > 0 else { + + removeObject(forKey: vpnProxyExcludedDomainsKey) + return + } + + set(newValue, forKey: vpnProxyExcludedDomainsKey) + } + } + + var vpnProxyExcludedDomainsPublisher: AnyPublisher<[String], Never> { + publisher(for: \.vpnProxyExcludedDomains).eraseToAnyPublisher() + } + + func resetVPNProxyExcludedDomains() { + removeObject(forKey: vpnProxyExcludedDomainsKey) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift new file mode 100644 index 0000000000..fdc7fb3177 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift @@ -0,0 +1,293 @@ +// +// TransparentProxyController.swift +// +// Copyright © 2024 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 Combine +import Foundation +import NetworkExtension +import NetworkProtection +import OSLog // swiftlint:disable:this enforce_os_log_wrapper +import PixelKit +import SystemExtensions + +/// Controller for ``TransparentProxyProvider`` +/// +@MainActor +public final class TransparentProxyController { + + public enum StartError: Error { + case attemptToStartWithoutBackingActiveFeatures + case couldNotRetrieveProtocolConfiguration + case couldNotEncodeSettingsSnapshot + case failedToLoadConfiguration(_ error: Error) + case failedToSaveConfiguration(_ error: Error) + case failedToStartProvider(_ error: Error) + } + + public typealias EventCallback = (Event) -> Void + public typealias ManagerSetupCallback = (_ manager: NETransparentProxyManager) async -> Void + + /// Dry mode means this won't really do anything to start or stop the proxy. + /// + /// This is useful for testing. + /// + private let dryMode: Bool + + /// The bundleID of the extension that contains the ``TransparentProxyProvider``. + /// + private let extensionID: String + + /// The event handler + /// + public var eventHandler: EventCallback? + + /// Callback to set up a ``NETransparentProxyManager``. + /// + public let setup: ManagerSetupCallback + + private var internalManager: NETransparentProxyManager? + + /// Whether the proxy settings should be stored in the provider configuration. + /// + /// We recommend setting this to true if the provider is running in a System Extension and can't access + /// shared `TransparentProxySettings`. If the provider is in an App Extension you should instead + /// use a shared `TransparentProxySettings` and set this to false. + /// + private let storeSettingsInProviderConfiguration: Bool + public let settings: TransparentProxySettings + private let notificationCenter: NotificationCenter + private var cancellables = Set() + + // MARK: - Initializers + + /// Default initializer. + /// + /// - Parameters: + /// - extensionID: the bundleID for the extension that contains the ``TransparentProxyProvider``. + /// This class DOES NOT take any responsibility in installing the system extension. It only uses + /// the extensionID to identify the appropriate manager configuration to load / save. + /// - storeSettingsInProviderConfiguration: whether the provider configuration will be used for storing + /// the proxy settings. Should be `true` when using a System Extension and `false` when using + /// an App Extension. + /// - settings: the settings to use for this proxy. + /// - dryMode: whether this class is initialized in dry mode. + /// - setup: a callback that will be called whenever a ``NETransparentProxyManager`` needs + /// to be setup. + /// + public init(extensionID: String, + storeSettingsInProviderConfiguration: Bool, + settings: TransparentProxySettings, + notificationCenter: NotificationCenter = .default, + dryMode: Bool = false, + setup: @escaping ManagerSetupCallback) { + + self.dryMode = dryMode + self.extensionID = extensionID + self.notificationCenter = notificationCenter + self.settings = settings + self.setup = setup + self.storeSettingsInProviderConfiguration = storeSettingsInProviderConfiguration + + subscribeToProviderConfigurationChanges() + subscribeToSettingsChanges() + } + + // MARK: - Relay Settings Changes + + private func subscribeToProviderConfigurationChanges() { + notificationCenter.publisher(for: .NEVPNConfigurationChange) + .receive(on: DispatchQueue.main) + .sink { _ in + self.reloadProviderConfiguration() + } + .store(in: &cancellables) + } + + private func reloadProviderConfiguration() { + Task { @MainActor in + try? await self.manager?.loadFromPreferences() + } + } + + private func subscribeToSettingsChanges() { + settings.changePublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: relay(_:)) + .store(in: &cancellables) + } + + private func relay(_ change: TransparentProxySettings.Change) { + Task { @MainActor in + guard let session = await session else { + return + } + + switch session.status { + case .connected, .connecting, .reasserting: + break + default: + return + } + + try TransparentProxySession(session).send(.changeSetting(change, responseHandler: { + // no-op + })) + } + } + + // MARK: - Setting up NETransparentProxyManager + + /// Loads a saved manager + /// + /// This is a bit of a hack that will be run just once for the instance. The reason we want this to run only once is that + /// `NETransparentProxyManager.loadAllFromPreferences()` has a bug where it triggers status change + /// notifications. If the code trying to retrieve the manager is the result of a notification, we may soon find outselves + /// in an infinite loop. + /// + private var triedLoadingManager = false + + /// Loads the configuration matching our ``extensionID``. + /// + public var manager: NETransparentProxyManager? { + get async { + if let internalManager { + return internalManager + } + + if !triedLoadingManager { + triedLoadingManager = true + + let manager = try? await NETransparentProxyManager.loadAllFromPreferences().first { manager in + (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == extensionID + } + self.internalManager = manager + } + + return internalManager + } + } + + /// Loads an existing configuration or creates a new one, if one doesn't exist. + /// + /// - Returns a properly configured `NETransparentProxyManager`. + /// + public func loadOrCreateConfiguration() async throws -> NETransparentProxyManager { + let manager = await manager ?? { + let manager = NETransparentProxyManager() + internalManager = manager + return manager + }() + + await setup(manager) + try setupAdditionalProviderConfiguration(manager) + + try await manager.saveToPreferences() + try await manager.loadFromPreferences() + + return manager + } + + private func setupAdditionalProviderConfiguration(_ manager: NETransparentProxyManager) throws { + guard storeSettingsInProviderConfiguration else { + return + } + + guard let providerProtocol = manager.protocolConfiguration as? NETunnelProviderProtocol else { + throw StartError.couldNotRetrieveProtocolConfiguration + } + + var providerConfiguration = providerProtocol.providerConfiguration ?? [String: Any]() + + guard let encodedSettings = try? JSONEncoder().encode(settings.snapshot()), + let encodedSettingsString = String(data: encodedSettings, encoding: .utf8) else { + + throw StartError.couldNotEncodeSettingsSnapshot + } + + providerConfiguration[TransparentProxySettingsSnapshot.key] = encodedSettingsString as NSString + providerProtocol.providerConfiguration = providerConfiguration + + } + + // MARK: - Connection & Session + + public var connection: NEVPNConnection? { + get async { + await manager?.connection + } + } + + public var session: NETunnelProviderSession? { + get async { + guard let manager = await manager, + let session = manager.connection as? NETunnelProviderSession else { + + // The active connection is not running, so there's no session, this is acceptable + return nil + } + + return session + } + } + + // MARK: - Connection + + public var status: NEVPNStatus { + get async { + await connection?.status ?? .disconnected + } + } + + // MARK: - Start & stop the proxy + + public var isRequiredForActiveFeatures: Bool { + settings.appRoutingRules.count > 0 || settings.excludedDomains.count > 0 + } + + public func start() async throws { + eventHandler?(.startInitiated) + + guard isRequiredForActiveFeatures else { + let error = StartError.attemptToStartWithoutBackingActiveFeatures + eventHandler?(.startFailure(error)) + throw error + } + + let manager: NETransparentProxyManager + + do { + manager = try await loadOrCreateConfiguration() + } catch { + eventHandler?(.startFailure(error)) + throw error + } + + do { + try manager.connection.startVPNTunnel(options: [:]) + } catch { + let error = StartError.failedToStartProvider(error) + eventHandler?(.startFailure(error)) + throw error + } + + eventHandler?(.startSuccess) + } + + public func stop() async { + await connection?.stopVPNTunnel() + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift new file mode 100644 index 0000000000..33b75fd73b --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift @@ -0,0 +1,389 @@ +// +// TransparentProxyProvider.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkExtension +import NetworkProtection +import os.log // swiftlint:disable:this enforce_os_log_wrapper +import SystemConfiguration + +open class TransparentProxyProvider: NETransparentProxyProvider { + + public enum StartError: Error { + case missingProviderConfiguration + case failedToUpdateNetworkSettings(underlyingError: Error) + } + + public typealias EventCallback = (Event) -> Void + public typealias LoadOptionsCallback = (_ options: [String: Any]?) throws -> Void + + static let dnsPort = 53 + + @TCPFlowActor + var tcpFlowManagers = Set() + + @UDPFlowActor + var udpFlowManagers = Set() + + private let monitor = nw_path_monitor_create() + var directInterface: nw_interface_t? + + private let bMonitor = NWPathMonitor() + var interface: NWInterface? + + public let configuration: Configuration + public let settings: TransparentProxySettings + + @MainActor + public var isRunning = false + + public var eventHandler: EventCallback? + private let logger: Logger + + private lazy var appMessageHandler = TransparentProxyAppMessageHandler(settings: settings) + + // MARK: - Init + + public init(settings: TransparentProxySettings, + configuration: Configuration, + logger: Logger) { + + self.configuration = configuration + self.logger = logger + self.settings = settings + + logger.debug("[+] \(String(describing: Self.self), privacy: .public)") + } + + deinit { + logger.debug("[-] \(String(describing: Self.self), privacy: .public)") + } + + private func loadProviderConfiguration() throws { + guard configuration.loadSettingsFromProviderConfiguration else { + return + } + + guard let providerConfiguration = (protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration, + let encodedSettingsString = providerConfiguration[TransparentProxySettingsSnapshot.key] as? String, + let encodedSettings = encodedSettingsString.data(using: .utf8) else { + + throw StartError.missingProviderConfiguration + } + + let snapshot = try JSONDecoder().decode(TransparentProxySettingsSnapshot.self, from: encodedSettings) + settings.apply(snapshot) + } + + @MainActor + public func updateNetworkSettings() async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @MainActor in + let networkSettings = makeNetworkSettings() + logger.log("Updating network settings: \(String(describing: networkSettings), privacy: .public)") + + setTunnelNetworkSettings(networkSettings) { [eventHandler, logger] error in + if let error { + logger.error("Failed to update network settings: \(String(describing: error), privacy: .public)") + eventHandler?(.failedToUpdateNetworkSettings(error)) + continuation.resume(throwing: error) + return + } + + logger.log("Successfully Updated network settings: \(String(describing: error), privacy: .public))") + continuation.resume() + } + } + } + } + + private func makeNetworkSettings() -> NETransparentProxyNetworkSettings { + let networkSettings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + + networkSettings.includedNetworkRules = [ + NENetworkRule(remoteNetwork: NWHostEndpoint(hostname: "127.0.0.1", port: ""), remotePrefix: 0, localNetwork: nil, localPrefix: 0, protocol: .any, direction: .outbound) + ] + + return networkSettings + } + + override public func startProxy(options: [String: Any]?, + completionHandler: @escaping (Error?) -> Void) { + + eventHandler?(.startInitiated) + + logger.log( + """ + Starting proxy\n + > configuration: \(String(describing: self.configuration), privacy: .public)\n + > settings: \(String(describing: self.settings), privacy: .public)\n + > options: \(String(describing: options), privacy: .public) + """) + + do { + try loadProviderConfiguration() + } catch { + logger.error("Failed to load provider configuration, bailing out") + eventHandler?(.startFailure(error)) + completionHandler(error) + return + } + + Task { @MainActor in + do { + startMonitoringNetworkInterfaces() + + try await updateNetworkSettings() + logger.log("Proxy started successfully") + isRunning = true + eventHandler?(.startSuccess) + completionHandler(nil) + } catch { + let error = StartError.failedToUpdateNetworkSettings(underlyingError: error) + logger.error("Proxy failed to start \(String(reflecting: error), privacy: .public)") + eventHandler?(.startFailure(error)) + completionHandler(error) + } + } + } + + override public func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + logger.log("Stopping proxy with reason: \(String(reflecting: reason), privacy: .public)") + + Task { @MainActor in + stopMonitoringNetworkInterfaces() + isRunning = false + completionHandler() + } + } + + override public func sleep(completionHandler: @escaping () -> Void) { + Task { @MainActor in + stopMonitoringNetworkInterfaces() + logger.log("The proxy is now sleeping") + completionHandler() + } + } + + override public func wake() { + Task { @MainActor in + logger.log("The proxy is now awake") + startMonitoringNetworkInterfaces() + } + } + + override public func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool { + guard let flow = flow as? NEAppProxyTCPFlow else { + logger.info("Expected a TCP flow, but got something else. We're ignoring it.") + return false + } + + guard let remoteEndpoint = flow.remoteEndpoint as? NWHostEndpoint, + !isDnsServer(remoteEndpoint) else { + return false + } + + let printableRemote = flow.remoteHostname ?? (flow.remoteEndpoint as? NWHostEndpoint)?.hostname ?? "unknown" + + logger.debug( + """ + [TCP] New flow: \(String(describing: flow), privacy: .public) + - remote: \(printableRemote, privacy: .public) + - flowID: \(String(describing: flow.metaData.filterFlowIdentifier?.uuidString), privacy: .public) + - appID: \(String(describing: flow.metaData.sourceAppSigningIdentifier), privacy: .public) + """) + + guard let interface else { + logger.error("[TCP: \(String(describing: flow), privacy: .public)] Expected an interface to exclude traffic through") + return false + } + + switch path(for: flow) { + case .block(let reason): + switch reason { + case .appRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Blocking traffic due to app rule") + case .domainRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Blocking traffic due to domain rule") + } + case .excludeFromVPN(let reason): + switch reason { + case .appRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Excluding traffic due to app rule") + case .domainRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Excluding traffic due to domain rule") + } + case .routeThroughVPN: + return false + } + + flow.networkInterface = directInterface + + Task { @TCPFlowActor in + let flowManager = TCPFlowManager(flow: flow) + tcpFlowManagers.insert(flowManager) + + try? await flowManager.start(interface: interface) + tcpFlowManagers.remove(flowManager) + } + + return true + } + + override public func handleNewUDPFlow(_ flow: NEAppProxyUDPFlow, initialRemoteEndpoint remoteEndpoint: NWEndpoint) -> Bool { + + guard let remoteEndpoint = remoteEndpoint as? NWHostEndpoint, + !isDnsServer(remoteEndpoint) else { + return false + } + + let printableRemote = remoteEndpoint.hostname + + logger.log( + """ + [UDP] New flow: \(String(describing: flow), privacy: .public) + - remote: \(printableRemote, privacy: .public) + - flowID: \(String(describing: flow.metaData.filterFlowIdentifier?.uuidString), privacy: .public) + - appID: \(String(describing: flow.metaData.sourceAppSigningIdentifier), privacy: .public) + """) + + guard let interface else { + logger.error("[UDP: \(String(describing: flow), privacy: .public)] Expected an interface to exclude traffic through") + return false + } + + switch path(for: flow) { + case .block(let reason): + switch reason { + case .appRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Blocking traffic due to app rule") + case .domainRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Blocking traffic due to domain rule") + } + case .excludeFromVPN(let reason): + switch reason { + case .appRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Excluding traffic due to app rule") + case .domainRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Excluding traffic due to domain rule") + } + case .routeThroughVPN: + return false + } + + flow.networkInterface = directInterface + + Task { @UDPFlowActor in + let flowManager = UDPFlowManager(flow: flow) + udpFlowManagers.insert(flowManager) + + try? await flowManager.start(interface: interface) + udpFlowManagers.remove(flowManager) + } + + return true + } + + // MARK: - Path Monitors + + @MainActor + private func startMonitoringNetworkInterfaces() { + bMonitor.pathUpdateHandler = { [weak self, logger] path in + logger.log("Available interfaces updated: \(String(reflecting: path.availableInterfaces), privacy: .public)") + + self?.interface = path.availableInterfaces.first { interface in + interface.type != .other + } + } + bMonitor.start(queue: .main) + + nw_path_monitor_set_queue(monitor, .main) + nw_path_monitor_set_update_handler(monitor) { [weak self, logger] path in + guard let self else { return } + + let interfaces = SCNetworkInterfaceCopyAll() + logger.log("Available interfaces updated: \(String(reflecting: interfaces), privacy: .public)") + + nw_path_enumerate_interfaces(path) { interface in + guard nw_interface_get_type(interface) != nw_interface_type_other else { + return true + } + + self.directInterface = interface + return false + } + } + + nw_path_monitor_start(monitor) + } + + @MainActor + private func stopMonitoringNetworkInterfaces() { + bMonitor.cancel() + nw_path_monitor_cancel(monitor) + } + + // MARK: - Ignoring DNS flows + + private func isDnsServer(_ endpoint: NWHostEndpoint) -> Bool { + Int(endpoint.port) == Self.dnsPort + } + + // MARK: - VPN exclusions logic + + private enum FlowPath { + case block(dueTo: Reason) + case excludeFromVPN(dueTo: Reason) + case routeThroughVPN + + enum Reason { + case appRule + case domainRule + } + } + + private func path(for flow: NEAppProxyFlow) -> FlowPath { + let appIdentifier = flow.metaData.sourceAppSigningIdentifier + + switch settings.appRoutingRules[appIdentifier] { + case .none: + if let hostname = flow.remoteHostname, + isExcludedDomain(hostname) { + return .excludeFromVPN(dueTo: .domainRule) + } + + return .routeThroughVPN + case .block: + return .block(dueTo: .appRule) + case .exclude: + return .excludeFromVPN(dueTo: .domainRule) + } + } + + private func isExcludedDomain(_ hostname: String) -> Bool { + settings.excludedDomains.contains { excludedDomain in + hostname.hasSuffix(excludedDomain) + } + } + + // MARK: - Communication with App + + override public func handleAppMessage(_ messageData: Data) async -> Data? { + await appMessageHandler.handle(messageData) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift new file mode 100644 index 0000000000..3e841faeca --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift @@ -0,0 +1,40 @@ +// +// TransparentProxyProviderConfiguration.swift +// +// Copyright © 2024 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 + +extension TransparentProxyProvider { + /// Configuration to define behaviour for the provider based on the parent process' + /// business domain. + /// + /// This should not be passed in the startup options dictionary. + /// + public struct Configuration { + /// Whether the proxy settings should be loaded from the provider configuration in the startup options. + /// + /// We recommend setting this to true if the provider is running in a System Extension and can't access + /// shared `TransparentProxySettings`. If the provider is in an App Extension you should instead + /// use a shared `TransparentProxySettings` and set this to false. + /// + let loadSettingsFromProviderConfiguration: Bool + + public init(loadSettingsFromProviderConfiguration: Bool) { + self.loadSettingsFromProviderConfiguration = loadSettingsFromProviderConfiguration + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index 66f9bb15ea..2575803866 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -54,9 +54,11 @@ public struct NetworkProtectionStatusView: View { PromptActionView(model: promptActionViewModel) .padding(.horizontal, 5) .padding(.top, 5) + .transition(.slide) } else { if let healthWarning = model.issueDescription { connectionHealthWarningView(message: healthWarning) + .transition(.slide) } } @@ -67,12 +69,14 @@ public struct NetworkProtectionStatusView: View { if model.showDebugInformation { DebugInformationView(model: DebugInformationViewModel()) + .transition(.slide) } bottomMenuView() } .padding(5) .frame(maxWidth: 350, alignment: .top) + .transition(.slide) } // MARK: - Composite Views diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 2a86e999d0..754ca81034 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -143,9 +143,9 @@ extension NetworkProtectionStatusView { onboardingStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] status in - self?.onboardingStatus = status - } - .store(in: &cancellables) + self?.onboardingStatus = status + } + .store(in: &cancellables) } func refreshLoginItemStatus() { @@ -184,14 +184,14 @@ extension NetworkProtectionStatusView { .subscribe(on: Self.tunnelErrorDispatchQueue) .sink { [weak self] errorMessage in - guard let self else { - return - } + guard let self else { + return + } - Task { @MainActor in - self.lastTunnelErrorMessage = errorMessage - } - }.store(in: &cancellables) + Task { @MainActor in + self.lastTunnelErrorMessage = errorMessage + } + }.store(in: &cancellables) } private func subscribeToControllerErrorMessages() { @@ -199,14 +199,14 @@ extension NetworkProtectionStatusView { .subscribe(on: Self.controllerErrorDispatchQueue) .sink { [weak self] errorMessage in - guard let self else { - return - } + guard let self else { + return + } - Task { @MainActor in - self.lastControllerErrorMessage = errorMessage - } - }.store(in: &cancellables) + Task { @MainActor in + self.lastControllerErrorMessage = errorMessage + } + }.store(in: &cancellables) } private func subscribeToDebugInformationChanges() { diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift new file mode 100644 index 0000000000..bd21fe50d1 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift @@ -0,0 +1,120 @@ +// +// TransparentProxyControllerPixelTests.swift +// +// Copyright © 2024 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 +@testable import NetworkProtectionProxy +import PixelKit +import PixelKitTestingUtilities +import XCTest + +extension TransparentProxyController.Event: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyController.Event, rhs: NetworkProtectionProxy.TransparentProxyController.Event) -> Bool { + + lhs.name == rhs.name && lhs.parameters == rhs.parameters + } + + public func hash(into hasher: inout Hasher) { + name.hash(into: &hasher) + parameters.hash(into: &hasher) + } +} + +extension TransparentProxyController.StartError: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyController.StartError, rhs: NetworkProtectionProxy.TransparentProxyController.StartError) -> Bool { + + let lhs = lhs as NSError + let rhs = rhs as NSError + + return lhs.code == rhs.code && lhs.domain == rhs.domain + } + + public func hash(into hasher: inout Hasher) { + (self as NSError).hash(into: &hasher) + (underlyingError as? NSError)?.hash(into: &hasher) + } +} + +final class TransparentProxyControllerPixelTests: XCTestCase { + + static let startFailureFullPixelName = "m_mac_vpn_proxy_controller_start_failure" + static let startInitiatedFullPixelName = "m_mac_vpn_proxy_controller_start_initiated" + static let startSuccessFullPixelName = "m_mac_vpn_proxy_controller_start_success" + + enum TestError: PixelKitEventErrorDetails { + case testError + + static let underlyingError = NSError(domain: "test", code: 1) + + var underlyingError: Error? { + Self.underlyingError + } + } + + // MARK: - Test Firing Pixels + + func testFiringPixelsWithoutParameters() { + let tests: [TransparentProxyController.Event: PixelFireExpectations] = [ + .startInitiated: PixelFireExpectations(pixelName: Self.startInitiatedFullPixelName), + .startSuccess: PixelFireExpectations(pixelName: Self.startSuccessFullPixelName) + ] + + for (event, expectations) in tests { + verifyThat(event, + meets: expectations, + file: #filePath, + line: #line) + } + } + + func testFiringStartFailures() { + // Just a convenience method to return the right expectation for each error + func expectaton(forError error: TransparentProxyController.StartError) -> PixelFireExpectations { + switch error { + case .attemptToStartWithoutBackingActiveFeatures, + .couldNotEncodeSettingsSnapshot, + .couldNotRetrieveProtocolConfiguration: + return PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: error) + case .failedToLoadConfiguration(let underlyingError), + .failedToSaveConfiguration(let underlyingError), + .failedToStartProvider(let underlyingError): + return PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: error, + underlyingError: underlyingError) + } + } + + let errors: [TransparentProxyController.StartError] = [ + .attemptToStartWithoutBackingActiveFeatures, + .couldNotEncodeSettingsSnapshot, + .couldNotRetrieveProtocolConfiguration, + .failedToLoadConfiguration(TestError.underlyingError), + .failedToSaveConfiguration(TestError.underlyingError), + .failedToStartProvider(TestError.underlyingError) + ] + + for error in errors { + verifyThat(TransparentProxyController.Event.startFailure(error), + meets: expectaton(forError: error), + file: #filePath, + line: #line) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift new file mode 100644 index 0000000000..faf31729a4 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift @@ -0,0 +1,66 @@ +// +// TransparentProxyProviderPixelTests.swift +// +// Copyright © 2024 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 +@testable import NetworkProtectionProxy +import PixelKit +import PixelKitTestingUtilities +import XCTest + +extension TransparentProxyProvider.Event: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyProvider.Event, rhs: NetworkProtectionProxy.TransparentProxyProvider.Event) -> Bool { + + lhs.name == rhs.name && lhs.parameters == rhs.parameters + } + + public func hash(into hasher: inout Hasher) { + name.hash(into: &hasher) + parameters.hash(into: &hasher) + } +} + +final class TransparentProxyProviderPixelTests: XCTestCase { + + static let startFailureFullPixelName = "m_mac_vpn_proxy_provider_start_failure" + static let startInitiatedFullPixelName = "m_mac_vpn_proxy_provider_start_initiated" + static let startSuccessFullPixelName = "m_mac_vpn_proxy_provider_start_success" + + enum TestError: Error { + case testError + } + + // MARK: - Test Firing Pixels + + func testFiringPixels() { + let tests: [TransparentProxyProvider.Event: PixelFireExpectations] = [ + .startInitiated: PixelFireExpectations(pixelName: Self.startInitiatedFullPixelName), + .startFailure(TestError.testError): + PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: TestError.testError), + .startSuccess: PixelFireExpectations(pixelName: Self.startSuccessFullPixelName) + ] + + for (event, expectations) in tests { + verifyThat(event, + meets: expectations, + file: #filePath, + line: #line) + } + } +} diff --git a/LocalPackages/PixelKit/Package.swift b/LocalPackages/PixelKit/Package.swift index 61de75cd32..1222931baa 100644 --- a/LocalPackages/PixelKit/Package.swift +++ b/LocalPackages/PixelKit/Package.swift @@ -20,7 +20,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift index ffd9d88796..7a25f133ff 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift @@ -94,11 +94,13 @@ public extension Error { let nsError = self as NSError params[PixelKit.Parameters.errorCode] = "\(nsError.code)" - params[PixelKit.Parameters.errorDesc] = nsError.domain + params[PixelKit.Parameters.errorDomain] = nsError.domain + params[PixelKit.Parameters.errorDesc] = nsError.localizedDescription if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { params[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.domain + params[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain + params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.localizedDescription } if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift index b13aea7b17..9c616d0060 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift @@ -275,11 +275,23 @@ public final class PixelKit { newParams = nil } + let newError: Error? + + if let event = event as? PixelKitEventV2 { + // For v2 events we only consider the error specified in the event + // and purposedly ignore the parameter in this call. + // This is to encourage moving the error over to the protocol error + // instead of still relying on the parameter of this call. + newError = event.error + } else { + newError = error + } + fire(pixelNamed: pixelName, frequency: frequency, withHeaders: headers, withAdditionalParameters: newParams, - withError: error, + withError: newError, allowedQueryReservedCharacters: allowedQueryReservedCharacters, includeAppVersionParameter: includeAppVersionParameter, onComplete: onComplete) @@ -365,8 +377,16 @@ extension Dictionary where Key == String, Value == String { self[PixelKit.Parameters.errorCode] = "\(nsError.code)" self[PixelKit.Parameters.errorDomain] = nsError.domain + self[PixelKit.Parameters.errorDesc] = nsError.localizedDescription + + if let error = error as? PixelKitEventErrorDetails, + let underlyingError = error.underlyingError { - if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { + let underlyingNSError = underlyingError as NSError + self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" + self[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain + self[PixelKit.Parameters.underlyingErrorDesc] = underlyingNSError.localizedDescription + } else 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 { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift index 83965ba999..bc87070df3 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift @@ -34,7 +34,7 @@ public final class DebugEvent: PixelKitEvent { } public let eventType: EventType - private let error: Error? + public let error: Error? public init(eventType: EventType, error: Error? = nil) { self.eventType = eventType diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift new file mode 100644 index 0000000000..7048519e32 --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift @@ -0,0 +1,70 @@ +// +// PixelKitEventV2.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 + +public protocol PixelKitEventErrorDetails: Error { + var underlyingError: Error? { get } +} + +extension PixelKitEventErrorDetails { + var underlyingErrorParameters: [String: String] { + guard let nsError = underlyingError as? NSError else { + return [:] + } + + return [ + PixelKit.Parameters.underlyingErrorCode: "\(nsError.code)", + PixelKit.Parameters.underlyingErrorDomain: nsError.domain, + PixelKit.Parameters.underlyingErrorDesc: nsError.localizedDescription + ] + } +} + +/// New version of this protocol that allows us to maintain backwards-compatibility with PixelKitEvent +/// +/// This new implementation seeks to unify the handling of standard pixel parameters inside PixelKit. +/// The starting example of how this can be useful is error parameter handling - this protocol allows +/// the implementer to speciy an error without having to know about the parametrization of the error. +/// +/// The reason this wasn't done directly in `PixelKitEvent` is to reduce the risk of breaking existing +/// pixels, and to allow us to migrate towards this incrementally. +/// +public protocol PixelKitEventV2: PixelKitEvent { + var error: Error? { get } +} + +extension PixelKitEventV2 { + var pixelParameters: [String: String] { + guard let error else { + return [:] + } + + let nsError = error as NSError + var parameters = [ + PixelKit.Parameters.errorCode: "\(nsError.code)", + PixelKit.Parameters.errorDomain: nsError.domain, + ] + + if let error = error as? PixelKitEventErrorDetails { + parameters.merge(error.underlyingErrorParameters, uniquingKeysWith: { $1 }) + } + + return parameters + } +} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift new file mode 100644 index 0000000000..067eee091e --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift @@ -0,0 +1,36 @@ +// +// PixelFireExpectations.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 + +/// Structure containing information about a pixel fire event. +/// +/// This is useful for test validation for libraries that rely on PixelKit, to make sure the pixels contain +/// all of the fields they are supposed to contain.. +/// +public struct PixelFireExpectations { + let pixelName: String + var error: Error? + var underlyingError: Error? + + public init(pixelName: String, error: Error? = nil, underlyingError: Error? = nil) { + self.pixelName = pixelName + self.error = error + self.underlyingError = underlyingError + } +} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift new file mode 100644 index 0000000000..5088ba1371 --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift @@ -0,0 +1,148 @@ +// +// XCTestCase+PixelKit.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 +@testable import PixelKit +import XCTest + +public extension XCTestCase { + + // MARK: - Parameters + + /// List of standard pixel parameters. + /// + /// This is useful to support filtering these parameters out if needed. + /// + private static var standardPixelParameters = [ + PixelKit.Parameters.appVersion, + PixelKit.Parameters.pixelSource, + PixelKit.Parameters.test + ] + + /// List of errror pixel parameters + /// + private static var errorPixelParameters = [ + PixelKit.Parameters.errorCode, + PixelKit.Parameters.errorDomain, + PixelKit.Parameters.errorDesc + ] + + /// List of underlying error pixel parameters + /// + private static var underlyingErrorPixelParameters = [ + PixelKit.Parameters.underlyingErrorCode, + PixelKit.Parameters.underlyingErrorDomain, + PixelKit.Parameters.underlyingErrorDesc + ] + + /// Filter out the standard parameters. + /// + private static func filterStandardPixelParameters(from parameters: [String: String]) -> [String: String] { + parameters.filter { element in + !standardPixelParameters.contains(element.key) + } + } + + static var pixelPlatformPrefix: String { +#if os(macOS) + return "m_mac_" +#else + // Intentionally left blank for now because PixelKit currently doesn't support + // other platforms, but if we decide to implement another platform this'll fail + // and indicate that we need a value here. +#endif + } + + func expectedParameters(for event: PixelKitEventV2) -> [String: String] { + var expectedParameters = [String: String]() + + if let error = event.error { + let nsError = error as NSError + expectedParameters[PixelKit.Parameters.errorCode] = "\(nsError.code)" + expectedParameters[PixelKit.Parameters.errorDomain] = nsError.domain + expectedParameters[PixelKit.Parameters.errorDesc] = nsError.localizedDescription + + if let underlyingError = (error as? PixelKitEventErrorDetails)?.underlyingError { + let underlyingNSError = underlyingError as NSError + expectedParameters[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" + expectedParameters[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain + expectedParameters[PixelKit.Parameters.underlyingErrorDesc] = underlyingNSError.localizedDescription + } + } + + return expectedParameters + } + + // MARK: - Misc Convenience + + private var userDefaults: UserDefaults { + UserDefaults(suiteName: "testing_\(UUID().uuidString)")! + } + + // MARK: - Pixel Firing Expectations + + /// Provides some snapshot of a fired pixel so that external libraries can validate all the expected info is included. + /// + /// This method also checks that there is internal consistency in the expected fields. + /// + func verifyThat(_ event: PixelKitEventV2, meets expectations: PixelFireExpectations, file: StaticString, line: UInt) { + + let expectedPixelName = Self.pixelPlatformPrefix + event.name + let expectedParameters = expectedParameters(for: event) + let callbackExecutedExpectation = expectation(description: "The PixelKit callback has been executed") + + PixelKit.setUp(dryRun: false, + appVersion: "1.0.5", + source: "test-app", + defaultHeaders: [:], + log: .disabled, + defaults: userDefaults) { firedPixelName, _, firedParameters, _, _, completion in + callbackExecutedExpectation.fulfill() + + let firedParameters = Self.filterStandardPixelParameters(from: firedParameters) + + // Internal validations + + XCTAssertEqual(firedPixelName, expectedPixelName, file: file, line: line) + XCTAssertEqual(firedParameters, expectedParameters, file: file, line: line) + + // Expectations + + XCTAssertEqual(firedPixelName, expectations.pixelName) + + if let error = expectations.error { + let nsError = error as NSError + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorCode], String(nsError.code), file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDomain], nsError.domain, file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDesc], nsError.localizedDescription, file: file, line: line) + } + + if let underlyingError = expectations.underlyingError { + let nsError = underlyingError as NSError + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorCode], String(nsError.code), file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDomain], nsError.domain, file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDesc], nsError.localizedDescription, file: file, line: line) + } + + completion(true, nil) + } + + PixelKit.fire(event) + waitForExpectations(timeout: 0.1) + } +} diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 197a2c4eee..e5192fc149 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SwiftUIExtensions/Package.swift b/LocalPackages/SwiftUIExtensions/Package.swift index 6f8c716bc3..10d667a750 100644 --- a/LocalPackages/SwiftUIExtensions/Package.swift +++ b/LocalPackages/SwiftUIExtensions/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library(name: "PreferencesViews", targets: ["PreferencesViews"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 678077f716..6da8332d6a 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(path: "../SwiftUIExtensions"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 188a15c3ad..43bafef378 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -16,7 +16,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/XPCHelper/Package.swift b/LocalPackages/XPCHelper/Package.swift index fb49a79f25..e62141ec7a 100644 --- a/LocalPackages/XPCHelper/Package.swift +++ b/LocalPackages/XPCHelper/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "XPCHelper", targets: ["XPCHelper"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), ], targets: [ .target( diff --git a/NetworkProtectionSystemExtension/Info.plist b/NetworkProtectionSystemExtension/Info.plist index c35c8be1ca..43844d5702 100644 --- a/NetworkProtectionSystemExtension/Info.plist +++ b/NetworkProtectionSystemExtension/Info.plist @@ -14,6 +14,8 @@ com.apple.networkextension.packet-tunnel $(PRODUCT_MODULE_NAME).MacPacketTunnelProvider + com.apple.networkextension.app-proxy + $(PRODUCT_MODULE_NAME).MacTransparentProxyProvider MAIN_BUNDLE_IDENTIFIER $(MAIN_BUNDLE_IDENTIFIER) diff --git a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements index a049fa6886..4252e67c8e 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements +++ b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.security.app-sandbox diff --git a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements index f7d87546d2..23068f001f 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements +++ b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.security.app-sandbox diff --git a/VPNProxyExtension/Info.plist b/VPNProxyExtension/Info.plist new file mode 100644 index 0000000000..7f2489c298 --- /dev/null +++ b/VPNProxyExtension/Info.plist @@ -0,0 +1,17 @@ + + + + + DISTRIBUTED_NOTIFICATIONS_PREFIX + $(DISTRIBUTED_NOTIFICATIONS_PREFIX) + NETP_APP_GROUP + $(NETP_APP_GROUP) + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.app-proxy + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).MacTransparentProxyProvider + + + diff --git a/VPNProxyExtension/VPNProxyExtension.entitlements b/VPNProxyExtension/VPNProxyExtension.entitlements new file mode 100644 index 0000000000..968c758f97 --- /dev/null +++ b/VPNProxyExtension/VPNProxyExtension.entitlements @@ -0,0 +1,25 @@ + + + + + com.apple.security.application-groups + + $(TeamIdentifierPrefix)com.duckduckgo.macos.browser.network-protection + $(NETP_APP_GROUP) + + com.apple.security.app-sandbox + + com.apple.security.network.server + + keychain-access-groups + + $(NETP_APP_GROUP) + + com.apple.developer.networking.networkextension + + app-proxy-provider + + com.apple.security.network.client + + + diff --git a/fastlane/Matchfile b/fastlane/Matchfile index 8dbddfccb8..2af9eed1fe 100644 --- a/fastlane/Matchfile +++ b/fastlane/Matchfile @@ -11,6 +11,8 @@ app_identifier [ "com.duckduckgo.mobile.ios.review", "com.duckduckgo.mobile.ios.vpn.agent.review", "com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension", + "com.duckduckgo.mobile.ios.vpn.agent.proxy", + "com.duckduckgo.mobile.ios.vpn.agent.review.proxy", "com.duckduckgo.mobile.ios.DBP.backgroundAgent.review", "com.duckduckgo.mobile.ios.DBP.backgroundAgent" diff --git a/scripts/assets/AppStoreExportOptions.plist b/scripts/assets/AppStoreExportOptions.plist index b9395f914a..9ebf3d7885 100644 --- a/scripts/assets/AppStoreExportOptions.plist +++ b/scripts/assets/AppStoreExportOptions.plist @@ -14,12 +14,16 @@ match AppStore com.duckduckgo.mobile.ios.vpn.agent macos com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension match AppStore com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension macos + com.duckduckgo.mobile.ios.vpn.agent.proxy + match AppStore com.duckduckgo.mobile.ios.vpn.agent.proxy macos com.duckduckgo.mobile.ios.review match AppStore com.duckduckgo.mobile.ios.review macos com.duckduckgo.mobile.ios.vpn.agent.review match AppStore com.duckduckgo.mobile.ios.vpn.agent.review macos com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension match AppStore com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension macos + com.duckduckgo.mobile.ios.vpn.agent.review.proxy + match AppStore com.duckduckgo.mobile.ios.vpn.agent.review.proxy macos com.duckduckgo.mobile.ios.DBP.backgroundAgent match AppStore com.duckduckgo.mobile.ios.DBP.backgroundAgent macos com.duckduckgo.mobile.ios.DBP.backgroundAgent.review From 7c2e3e4057681ecacb972eb8d7eecda0f3762c12 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:01:25 +0100 Subject: [PATCH 08/12] add bundle to sync strings (#2232) Task/Issue URL: https://app.asana.com/0/0/1206643467042714/f **Description**: Adds bundle to sync strings **Steps to test this PR**: 1. Check the app builds as expected --- LocalPackages/SyncUI/Package.swift | 3 + .../Localizable.xcstrings | 8 +- .../Sources/SyncUI/internal/UserText.swift | 197 +++++++++--------- 3 files changed, 106 insertions(+), 102 deletions(-) rename LocalPackages/SyncUI/Sources/SyncUI/{internal => Resources}/Localizable.xcstrings (99%) diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 6da8332d6a..b927525124 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -23,6 +23,9 @@ let package = Package( .product(name: "PreferencesViews", package: "SwiftUIExtensions"), .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions") ], + resources: [ + .process("Resources") + ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) ], diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings similarity index 99% rename from LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings rename to LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings index abcf797258..340db4147a 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings +++ b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings @@ -209,7 +209,7 @@ } }, "paste-from-clipboard" : { - "comment" : "Paste button", + "comment" : "Paste from Clipboard button", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -245,7 +245,7 @@ } }, "preferences.begin-sync.card-footer" : { - "comment" : "Footer / captoin on the Begin Syncing card in sync settings", + "comment" : "Footer / caption on the Begin Syncing card in sync settings", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -353,7 +353,7 @@ } }, "preferences.preparing-to-sync.dialog-title" : { - "comment" : "Peparing to sync dialog title during sync set up", + "comment" : "Preparing to sync dialog title during sync set up", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -389,7 +389,7 @@ } }, "preferences.recover-synced-data.dialog-subtitle" : { - "comment" : "Recover synced data during Sync revoery process dialog subtitle", + "comment" : "Recover synced data during Sync recovery process dialog subtitle", "extractionState" : "extracted_with_value", "localizations" : { "en" : { diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift index ca0babbe0f..3d9356af3e 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift @@ -21,150 +21,151 @@ import Foundation enum UserText { // Generic Buttons - static let ok = NSLocalizedString("ok", value: "OK", comment: "OK button") - static let notNow = NSLocalizedString("notnow", value: "Not Now", comment: "Not Now button") - static let cancel = NSLocalizedString("cancel", value: "Cancel", comment: "Cancel button") - static let submit = NSLocalizedString("submit", value: "Submit", comment: "Submit button") - static let next = NSLocalizedString("next", value: "Next", comment: "Next button") - static let copy = NSLocalizedString("copy", value: "Copy", comment: "Copy button") - static let share = NSLocalizedString("share", value: "Share", comment: "Share button") - static let paste = NSLocalizedString("paste", value: "Paste", comment: "Paste button") - static let pasteFromClipboard = NSLocalizedString("paste-from-clipboard", value: "Paste from Clipboard", comment: "Paste button") - static let done = NSLocalizedString("done", value: "Done", comment: "Done button") + static let ok = NSLocalizedString("ok", bundle: Bundle.module, value: "OK", comment: "OK button") + static let notNow = NSLocalizedString("notnow", bundle: Bundle.module, value: "Not Now", comment: "Not Now button") + static let cancel = NSLocalizedString("cancel", bundle: Bundle.module, value: "Cancel", comment: "Cancel button") + static let submit = NSLocalizedString("submit", bundle: Bundle.module, value: "Submit", comment: "Submit button") + static let next = NSLocalizedString("next", bundle: Bundle.module, value: "Next", comment: "Next button") + static let copy = NSLocalizedString("copy", bundle: Bundle.module, value: "Copy", comment: "Copy button") + static let share = NSLocalizedString("share", bundle: Bundle.module, value: "Share", comment: "Share button") + static let paste = NSLocalizedString("paste", bundle: Bundle.module, value: "Paste", comment: "Paste button") + static let pasteFromClipboard = NSLocalizedString("paste-from-clipboard", bundle: Bundle.module, value: "Paste from Clipboard", comment: "Paste from Clipboard button") + static let done = NSLocalizedString("done", bundle: Bundle.module, value: "Done", comment: "Done button") // Sync Set Up View // Begin Sync card - static let beginSyncTitle = NSLocalizedString("preferences.begin-sync.card-title", value: "Begin Syncing", comment: "Begin Syncing card title in sync settings") - static let beginSyncDescription = NSLocalizedString("preferences.begin-sync.card-description", value: "Securely sync bookmarks and passwords between your devices.", comment: "Begin Syncing card description in sync settings") - static let beginSyncButton = NSLocalizedString("preferences.begin-sync.card-button", value: "Sync With Another Device", comment: "Button text on the Begin Syncing card in sync settings") - static let beginSyncFooter = NSLocalizedString("preferences.begin-sync.card-footer", value: "Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key.", comment: "Footer / captoin on the Begin Syncing card in sync settings") + static let beginSyncTitle = NSLocalizedString("preferences.begin-sync.card-title", bundle: Bundle.module, value: "Begin Syncing", comment: "Begin Syncing card title in sync settings") + static let beginSyncDescription = NSLocalizedString("preferences.begin-sync.card-description", bundle: Bundle.module, value: "Securely sync bookmarks and passwords between your devices.", comment: "Begin Syncing card description in sync settings") + static let beginSyncButton = NSLocalizedString("preferences.begin-sync.card-button", bundle: Bundle.module, value: "Sync With Another Device", comment: "Button text on the Begin Syncing card in sync settings") + static let beginSyncFooter = NSLocalizedString("preferences.begin-sync.card-footer", bundle: Bundle.module, value: "Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key.", comment: "Footer / caption on the Begin Syncing card in sync settings") + // Options - static let otherOptionsSectionTitle = NSLocalizedString("preferences.other-options.section-title", value: "Other Options", comment: "Sync settings. Other Options section title") - static let syncThisDeviceLink = NSLocalizedString("preferences.sync-this-device.link-title", value: "Sync and Back Up This Device", comment: "Sync settings. Title of a link to start setting up sync and backup the device") - static let recoverDataLink = NSLocalizedString("preferences.recover-data.link-title", value: "Recover Synced Data", comment: "Sync settings. Link to recover synced data.") + static let otherOptionsSectionTitle = NSLocalizedString("preferences.other-options.section-title", bundle: Bundle.module, value: "Other Options", comment: "Sync settings. Other Options section title") + static let syncThisDeviceLink = NSLocalizedString("preferences.sync-this-device.link-title", bundle: Bundle.module, value: "Sync and Back Up This Device", comment: "Sync settings. Title of a link to start setting up sync and backup the device") + static let recoverDataLink = NSLocalizedString("preferences.recover-data.link-title", bundle: Bundle.module, value: "Recover Synced Data", comment: "Sync settings. Link to recover synced data.") // Preparing to sync dialog - static let preparingToSyncDialogTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-title", value: "Preparing To Sync", comment: "Peparing to sync dialog title during sync set up") - static let preparingToSyncDialogSubTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-subtitle", value: "We're setting up the connection to synchronize your bookmarks and saved logins with the other device.", comment: "Preparing to sync dialog subtitle during sync set up") - static let preparingToSyncDialogAction = NSLocalizedString("preferences.preparing-to-sync.dialog-action", value: "Connecting…", comment: "Sync preparing to sync dialog action") + static let preparingToSyncDialogTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-title", bundle: Bundle.module, value: "Preparing To Sync", comment: "Preparing to sync dialog title during sync set up") + static let preparingToSyncDialogSubTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-subtitle", bundle: Bundle.module, value: "We're setting up the connection to synchronize your bookmarks and saved logins with the other device.", comment: "Preparing to sync dialog subtitle during sync set up") + static let preparingToSyncDialogAction = NSLocalizedString("preferences.preparing-to-sync.dialog-action", bundle: Bundle.module, value: "Connecting…", comment: "Sync preparing to sync dialog action") // Enter recovery code dialog - static let enterRecoveryCodeDialogTitle = NSLocalizedString("preferences.enter-recovery-code.dialog-title", value: "Enter Code", comment: "Sync enter recovery code dialog title") - static let enterRecoveryCodeDialogSubtitle = NSLocalizedString("preferences.enter-recovery-code.dialog-subtitle", value: "Enter the code on your Recovery PDF, or another synced device, to recover your synced data.", comment: "Sync enter recovery code dialog subtitle") - static let enterRecoveryCodeDialogAction1 = NSLocalizedString("preferences.enter-recovery-code.dialog-action1", value: "Paste Code Here", comment: "Sync enter recovery code dialog first possible action") - static let enterRecoveryCodeDialogAction2 = NSLocalizedString("preferences.enter-recovery-code.dialog-action2", value: "or scan QR code with a device that is still connected", comment: "Sync enter recovery code dialog second possible action") + static let enterRecoveryCodeDialogTitle = NSLocalizedString("preferences.enter-recovery-code.dialog-title", bundle: Bundle.module, value: "Enter Code", comment: "Sync enter recovery code dialog title") + static let enterRecoveryCodeDialogSubtitle = NSLocalizedString("preferences.enter-recovery-code.dialog-subtitle", bundle: Bundle.module, value: "Enter the code on your Recovery PDF, or another synced device, to recover your synced data.", comment: "Sync enter recovery code dialog subtitle") + static let enterRecoveryCodeDialogAction1 = NSLocalizedString("preferences.enter-recovery-code.dialog-action1", bundle: Bundle.module, value: "Paste Code Here", comment: "Sync enter recovery code dialog first possible action") + static let enterRecoveryCodeDialogAction2 = NSLocalizedString("preferences.enter-recovery-code.dialog-action2", bundle: Bundle.module, value: "or scan QR code with a device that is still connected", comment: "Sync enter recovery code dialog second possible action") // Recover synced data dialog - static let reciverSyncedDataDialogTitle = NSLocalizedString("preferences.recover-synced-data.dialog-title", value: "Recover Synced Data", comment: "Sync recover synced data dialog title") - static let reciverSyncedDataDialogSubitle = NSLocalizedString("preferences.recover-synced-data.dialog-subtitle", value: "To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync.", comment: "Recover synced data during Sync revoery process dialog subtitle") - static let reciverSyncedDataDialogButton = NSLocalizedString("preferences.recover-synced-data.dialog-button", value: "Get Started", comment: "Sync recover synced data dialog button") + static let reciverSyncedDataDialogTitle = NSLocalizedString("preferences.recover-synced-data.dialog-title", bundle: Bundle.module, value: "Recover Synced Data", comment: "Sync recover synced data dialog title") + static let reciverSyncedDataDialogSubitle = NSLocalizedString("preferences.recover-synced-data.dialog-subtitle", bundle: Bundle.module, value: "To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync.", comment: "Recover synced data during Sync recovery process dialog subtitle") + static let reciverSyncedDataDialogButton = NSLocalizedString("preferences.recover-synced-data.dialog-button", bundle: Bundle.module, value: "Get Started", comment: "Sync recover synced data dialog button") // Sync Title - static let sync = NSLocalizedString("preferences.sync", value: "Sync & Backup", comment: "Show sync preferences") - static let syncRollOutBannerDescription = NSLocalizedString("preferences.sync.rollout-banner.description", value: "Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices.", comment: "Description of rollout banner") + static let sync = NSLocalizedString("preferences.sync", bundle: Bundle.module, value: "Sync & Backup", comment: "Show sync preferences") + static let syncRollOutBannerDescription = NSLocalizedString("preferences.sync.rollout-banner.description", bundle: Bundle.module, value: "Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices.", comment: "Description of rollout banner") - static let turnOff = NSLocalizedString("preferences.sync.turn-off", value: "Turn Off", comment: "Turn off sync confirmation dialog button title") - static let turnOffSync = NSLocalizedString("preferences.sync.turn-off.ellipsis", value: "Turn Off Sync…", comment: "Disable sync button caption") + static let turnOff = NSLocalizedString("preferences.sync.turn-off", bundle: Bundle.module, value: "Turn Off", comment: "Turn off sync confirmation dialog button title") + static let turnOffSync = NSLocalizedString("preferences.sync.turn-off.ellipsis", bundle: Bundle.module, value: "Turn Off Sync…", comment: "Disable sync button caption") // Sync Enabled View // Turn off sync dialog - static let turnOffSyncConfirmTitle = NSLocalizedString("preferences.sync.turn-off.confirm.title", value: "Turn off sync?", comment: "Turn off sync confirmation dialog title") - static let turnOffSyncConfirmMessage = NSLocalizedString("preferences.sync.turn-off.confirm.message", value: "This device will no longer be able to access your synced data.", comment: "Turn off sync confirmation dialog message") + static let turnOffSyncConfirmTitle = NSLocalizedString("preferences.sync.turn-off.confirm.title", bundle: Bundle.module, value: "Turn off sync?", comment: "Turn off sync confirmation dialog title") + static let turnOffSyncConfirmMessage = NSLocalizedString("preferences.sync.turn-off.confirm.message", bundle: Bundle.module, value: "This device will no longer be able to access your synced data.", comment: "Turn off sync confirmation dialog message") // Delete server data - static let turnOffAndDeleteServerData = NSLocalizedString("preferences.sync.turn-off-and-delete-data", value: "Turn Off and Delete Server Data…", comment: "Disable and delete data sync button caption") + static let turnOffAndDeleteServerData = NSLocalizedString("preferences.sync.turn-off-and-delete-data", bundle: Bundle.module, value: "Turn Off and Delete Server Data…", comment: "Disable and delete data sync button caption") // sync connected - static let syncConnected = NSLocalizedString("preferences.sync.connected", value: "Sync Enabled", comment: "Sync state is enabled") + static let syncConnected = NSLocalizedString("preferences.sync.connected", bundle: Bundle.module, value: "Sync Enabled", comment: "Sync state is enabled") // synced devices - static let syncedDevices = NSLocalizedString("preferences.sync.synced-devices", value: "Synced Devices", comment: "Settings section title") - static let thisDevice = NSLocalizedString("preferences.sync.this-device", value: "This Device", comment: "Indicator of a current user's device on the list") - static let currentDeviceDetails = NSLocalizedString("preferences.sync.current-device-details", value: "Details...", comment: "Sync Settings device details button") - static let removeDeviceButton = NSLocalizedString("preferences.sync.remove-device", value: "Remove...", comment: "Button to remove a device") + static let syncedDevices = NSLocalizedString("preferences.sync.synced-devices", bundle: Bundle.module, value: "Synced Devices", comment: "Settings section title") + static let thisDevice = NSLocalizedString("preferences.sync.this-device", bundle: Bundle.module, value: "This Device", comment: "Indicator of a current user's device on the list") + static let currentDeviceDetails = NSLocalizedString("preferences.sync.current-device-details", bundle: Bundle.module, value: "Details...", comment: "Sync Settings device details button") + static let removeDeviceButton = NSLocalizedString("preferences.sync.remove-device", bundle: Bundle.module, value: "Remove...", comment: "Button to remove a device") // Remove device dialog - static let removeDeviceConfirmTitle = NSLocalizedString("preferences.sync.remove-device-title", value: "Remove device?", comment: "Title on remove a device confirmation") - static let removeDeviceConfirmButton = NSLocalizedString("preferences.sync.remove-device-button", value: "Remove Device", comment: "Button text on remove a device confirmation button") + static let removeDeviceConfirmTitle = NSLocalizedString("preferences.sync.remove-device-title", bundle: Bundle.module, value: "Remove device?", comment: "Title on remove a device confirmation") + static let removeDeviceConfirmButton = NSLocalizedString("preferences.sync.remove-device-button", bundle: Bundle.module, value: "Remove Device", comment: "Button text on remove a device confirmation button") static func removeDeviceConfirmMessage(_ deviceName: String) -> String { let localized = NSLocalizedString("preferences.sync.remove-device-message", - value: "\"%@\" will no longer be able to access your synced data.", + bundle: Bundle.module, value: "\"%@\" will no longer be able to access your synced data.", comment: "Message to confirm the device will no longer be able to access the synced data - devoce name item inserted") return String(format: localized, deviceName) } - static let recovery = NSLocalizedString("prefrences.sync.recovery", value: "Recovery", comment: "Sync settings section title") - static let recoveryInstructions = NSLocalizedString("prefrences.sync.recovery-instructions", value: "If you lose your device, you will need this recovery code to restore your synced data.", comment: "Instructions on how to restore synced data") + static let recovery = NSLocalizedString("prefrences.sync.recovery", bundle: Bundle.module, value: "Recovery", comment: "Sync settings section title") + static let recoveryInstructions = NSLocalizedString("prefrences.sync.recovery-instructions", bundle: Bundle.module, value: "If you lose your device, you will need this recovery code to restore your synced data.", comment: "Instructions on how to restore synced data") // Sync with another device dialog - static let syncWithAnotherDeviceTitle = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-title", value: "Sync With Another Device", comment: "Sync with another device dialog title") + static let syncWithAnotherDeviceTitle = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-title", bundle: Bundle.module, value: "Sync With Another Device", comment: "Sync with another device dialog title") static func syncWithAnotherDeviceSubtitle(syncMenuPath: String) -> String { - let localized = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-subtitle1", value: "Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device.", comment: "Sync with another device dialog subtitle - Instruction with sync menu path item inserted") + let localized = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-subtitle1", bundle: Bundle.module, value: "Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device.", comment: "Sync with another device dialog subtitle - Instruction with sync menu path item inserted") return String(format: localized, syncMenuPath) } - static let syncMenuPath = NSLocalizedString("sync.menu.path", value: "Settings › Sync & Backup", comment: "Sync Menu Path") - static let syncWithAnotherDeviceShowCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-button", value: "Show Code", comment: "Text on show code button on Sync with another device dialog") - static let syncWithAnotherDeviceEnterCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-button", value: "Enter Code", comment: "Text on enter code button on Sync with another device dialog") - static let syncWithAnotherDeviceShowQRCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-qr-code-explanation", value: "Scan this QR code to connect.", comment: "Sync with another device dialog show qr code explanation") - static let syncWithAnotherDeviceEnterCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-explanation", value: "Paste the code here to sync.", comment: "Sync with another device dialog enter code explanation") - static let syncWithAnotherDeviceShowCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-explanation", value: "Share this code to connect with a desktop machine.", comment: "Sync with another device dialog show code explanation") - static let syncWithAnotherDeviceViewQRCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-qr-code-link", value: "View QR Code", comment: "Sync with another device dialog view qr code link") - static let syncWithAnotherDeviceViewTextCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-text-code-link", value: "View Text Code", comment: "Sync with another device dialog view text code link") + static let syncMenuPath = NSLocalizedString("sync.menu.path", bundle: Bundle.module, value: "Settings › Sync & Backup", comment: "Sync Menu Path") + static let syncWithAnotherDeviceShowCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-button", bundle: Bundle.module, value: "Show Code", comment: "Text on show code button on Sync with another device dialog") + static let syncWithAnotherDeviceEnterCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-button", bundle: Bundle.module, value: "Enter Code", comment: "Text on enter code button on Sync with another device dialog") + static let syncWithAnotherDeviceShowQRCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-qr-code-explanation", bundle: Bundle.module, value: "Scan this QR code to connect.", comment: "Sync with another device dialog show qr code explanation") + static let syncWithAnotherDeviceEnterCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-explanation", bundle: Bundle.module, value: "Paste the code here to sync.", comment: "Sync with another device dialog enter code explanation") + static let syncWithAnotherDeviceShowCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-explanation", bundle: Bundle.module, value: "Share this code to connect with a desktop machine.", comment: "Sync with another device dialog show code explanation") + static let syncWithAnotherDeviceViewQRCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-qr-code-link", bundle: Bundle.module, value: "View QR Code", comment: "Sync with another device dialog view qr code link") + static let syncWithAnotherDeviceViewTextCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-text-code-link", bundle: Bundle.module, value: "View Text Code", comment: "Sync with another device dialog view text code link") // Save recovery PDF dialog - static let saveRecoveryPDF = NSLocalizedString("prefrences.sync.save-recovery-pdf", value: "Save Your Recovery Code", comment: "Caption for a button to save Sync recovery PDF") - static let recoveryPDFExplanation = NSLocalizedString("prefrences.sync.recovery-pdf-explanation", value: "If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF.", comment: "Sync recovery PDF explanation") - static let recoveryPDFCopyCodeButton = NSLocalizedString("prefrences.sync.recovery-pdf-copy-code-button", value: "Copy Code", comment: "Sync recovery PDF copy code button") - static let recoveryPDFSavePDFButton = NSLocalizedString("prefrences.sync.recovery-pdf-save-pdf-button", value: "Save PDF", comment: "Sync recovery PDF save pdf button") - static let recoveryPDFWarning = NSLocalizedString("prefrences.sync.recovery-pdf-warning", value: "Anyone with access to this code can access your synced data, so please keep it in a safe place.", comment: "Sync recovery PDF warning") + static let saveRecoveryPDF = NSLocalizedString("prefrences.sync.save-recovery-pdf", bundle: Bundle.module, value: "Save Your Recovery Code", comment: "Caption for a button to save Sync recovery PDF") + static let recoveryPDFExplanation = NSLocalizedString("prefrences.sync.recovery-pdf-explanation", bundle: Bundle.module, value: "If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF.", comment: "Sync recovery PDF explanation") + static let recoveryPDFCopyCodeButton = NSLocalizedString("prefrences.sync.recovery-pdf-copy-code-button", bundle: Bundle.module, value: "Copy Code", comment: "Sync recovery PDF copy code button") + static let recoveryPDFSavePDFButton = NSLocalizedString("prefrences.sync.recovery-pdf-save-pdf-button", bundle: Bundle.module, value: "Save PDF", comment: "Sync recovery PDF save pdf button") + static let recoveryPDFWarning = NSLocalizedString("prefrences.sync.recovery-pdf-warning", bundle: Bundle.module, value: "Anyone with access to this code can access your synced data, so please keep it in a safe place.", comment: "Sync recovery PDF warning") // Sync with server dialog - static let syncWithServerTitle = NSLocalizedString("preferences.sync.sync-with-server-title", value: "Sync and Back Up This Device", comment: "Sync with server dialog title") - static let syncWithServerSubtitle1 = NSLocalizedString("preferences.sync.sync-with-server-subtitle1", value: "This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices.", comment: "Sync with server dialog first subtitle") - static let syncWithServerSubtitle2 = NSLocalizedString("preferences.sync.sync-with-server-subtitle2", value: "The encryption key is only stored on your device, DuckDuckGo cannot access it.", comment: "Sync with server dialog second subtitle") - static let syncWithServerButton = NSLocalizedString("preferences.sync.sync-with-server-button", value: "Turn On Sync & Backup", comment: "Sync with server dialog button") + static let syncWithServerTitle = NSLocalizedString("preferences.sync.sync-with-server-title", bundle: Bundle.module, value: "Sync and Back Up This Device", comment: "Sync with server dialog title") + static let syncWithServerSubtitle1 = NSLocalizedString("preferences.sync.sync-with-server-subtitle1", bundle: Bundle.module, value: "This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices.", comment: "Sync with server dialog first subtitle") + static let syncWithServerSubtitle2 = NSLocalizedString("preferences.sync.sync-with-server-subtitle2", bundle: Bundle.module, value: "The encryption key is only stored on your device, DuckDuckGo cannot access it.", comment: "Sync with server dialog second subtitle") + static let syncWithServerButton = NSLocalizedString("preferences.sync.sync-with-server-button", bundle: Bundle.module, value: "Turn On Sync & Backup", comment: "Sync with server dialog button") // Device synced dialog - static let deviceSynced = NSLocalizedString("prefrences.sync.device-synced", value: "Your data is synced!", comment: "Sync setup confirmation dialog title") + static let deviceSynced = NSLocalizedString("prefrences.sync.device-synced", bundle: Bundle.module, value: "Your data is synced!", comment: "Sync setup confirmation dialog title") // Device details - static let deviceDetailsTitle = NSLocalizedString("prefrences.sync.device-details.title", value: "Device Details", comment: "The title of the device details dialog") - static let deviceDetailsLabel = NSLocalizedString("prefrences.sync.device-details.label", value: "Name", comment: "The text entry label to name the device") - static let deviceDetailsPrompt = NSLocalizedString("prefrences.sync.device-details.prompt", value: "Device name", comment: "The text entry prompt to name the device") + static let deviceDetailsTitle = NSLocalizedString("prefrences.sync.device-details.title", bundle: Bundle.module, value: "Device Details", comment: "The title of the device details dialog") + static let deviceDetailsLabel = NSLocalizedString("prefrences.sync.device-details.label", bundle: Bundle.module, value: "Name", comment: "The text entry label to name the device") + static let deviceDetailsPrompt = NSLocalizedString("prefrences.sync.device-details.prompt", bundle: Bundle.module, value: "Device name", comment: "The text entry prompt to name the device") // Delete Account Dialog - static let deleteAccountTitle = NSLocalizedString("prefrences.sync.delete-account.title", value: "Delete server data?", comment: "Title for delete account confirmation pop up") - static let deleteAccountMessage = NSLocalizedString("prefrences.sync.delete-account.message", value: "These devices will be disconnected and your synced data will be deleted from the server.", comment: "Message for delete account confirmation pop up") - static let deleteAccountButton = NSLocalizedString("prefrences.sync.delete-account.button", value: "Delete Data", comment: "Label for delete account button") + static let deleteAccountTitle = NSLocalizedString("prefrences.sync.delete-account.title", bundle: Bundle.module, value: "Delete server data?", comment: "Title for delete account confirmation pop up") + static let deleteAccountMessage = NSLocalizedString("prefrences.sync.delete-account.message", bundle: Bundle.module, value: "These devices will be disconnected and your synced data will be deleted from the server.", comment: "Message for delete account confirmation pop up") + static let deleteAccountButton = NSLocalizedString("prefrences.sync.delete-account.button", bundle: Bundle.module, value: "Delete Data", comment: "Label for delete account button") // Sync enabled options - static let optionsSectionTitle = NSLocalizedString("prefrences.sync.options-section-title", value: "Options", comment: "Title for options settings") - static let shareFavoritesOptionTitle = NSLocalizedString("prefrences.sync.share-favorite-option-title", value: "Unify Favorites Across Devices", comment: "Title for share favorite option") - static let shareFavoritesOptionCaption = NSLocalizedString("prefrences.sync.share-favorite-option-caption", value: "Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate.", comment: "Caption for share favorite option") - static let fetchFaviconsOptionTitle = NSLocalizedString("prefrences.sync.fetch-favicons-option-title", value: "Auto-Download Icons", comment: "Title for fetch favicons option") - static let fetchFaviconsOptionCaption = NSLocalizedString("prefrences.sync.fetch-favicons-option-caption", value: "Automatically download icons for synced bookmarks. Icon downloads are exposed to your network.", comment: "Caption for fetch favicons option") + static let optionsSectionTitle = NSLocalizedString("prefrences.sync.options-section-title", bundle: Bundle.module, value: "Options", comment: "Title for options settings") + static let shareFavoritesOptionTitle = NSLocalizedString("prefrences.sync.share-favorite-option-title", bundle: Bundle.module, value: "Unify Favorites Across Devices", comment: "Title for share favorite option") + static let shareFavoritesOptionCaption = NSLocalizedString("prefrences.sync.share-favorite-option-caption", bundle: Bundle.module, value: "Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate.", comment: "Caption for share favorite option") + static let fetchFaviconsOptionTitle = NSLocalizedString("prefrences.sync.fetch-favicons-option-title", bundle: Bundle.module, value: "Auto-Download Icons", comment: "Title for fetch favicons option") + static let fetchFaviconsOptionCaption = NSLocalizedString("prefrences.sync.fetch-favicons-option-caption", bundle: Bundle.module, value: "Automatically download icons for synced bookmarks. Icon downloads are exposed to your network.", comment: "Caption for fetch favicons option") // sync enabled errors - static let syncLimitExceededTitle = NSLocalizedString("prefrences.sync.limit-exceeded-title", value: "Sync Paused", comment: "Title for sync limits exceeded warning") - 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 go to manage bookmarks") - static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", value: "Manage passwords…", comment: "Button title for sync credentials limits exceeded warning to go to manage passwords") - static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", value: "Sync & Backup Error", comment: "Title for sync error alert") - static let unableToSyncToServerDescription = NSLocalizedString("alert.unable-to-sync-to-server-description", value: "Unable to connect to the server.", comment: "Description for unable to sync to server error") - static let unableToSyncWithAnotherDeviceDescription = NSLocalizedString("alert.unable-to-sync-with-another-device-description", value: "Unable to Sync with another device.", comment: "Description for unable to sync with another device error") - static let unableToMergeTwoAccountsDescription = NSLocalizedString("alert.unable-to-merge-two-accounts-description", value: "To pair these devices, turn off Sync & Backup on one device then tap \"Sync With Another Device\" on the other device.", comment: "Description for unable to merge two accounts error") - static let unableToUpdateDeviceNameDescription = NSLocalizedString("alert.unable-to-update-device-name-description", value: "Unable to update the device name.", comment: "Description for unable to update device name error") - static let unableToTurnSyncOffDescription = NSLocalizedString("alert.unable-to-turn-sync-off-description", value: "Unable to turn Sync & Backup off.", comment: "Description for unable to turn sync off error") - static let unableToDeleteDataDescription = NSLocalizedString("alert.unable-to-delete-data-description", value: "Unable to delete data on the server.", comment: "Description for unable to delete data error") - static let unableToRemoveDeviceDescription = NSLocalizedString("alert.unable-to-remove-device-description", value: "Unable to remove this device from Sync & Backup.", comment: "Description for unable to remove device error") - static let invalidCodeDescription = NSLocalizedString("alert.invalid-code-description", value: "Sorry, this code is invalid. Please make sure it was entered correctly.", comment: "Description for invalid code error") - static let unableCreateRecoveryPdfDescription = NSLocalizedString("alert.unable-to-create-recovery-pdf-description", value: "Unable to create the recovery PDF.", comment: "Description for unable to create recovery pdf error") - - static let fetchFaviconsOnboardingTitle = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-title", value: "Download Missing Icons?", comment: "Title for fetch favicons onboarding dialog") - static let fetchFaviconsOnboardingMessage = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-message", value: "Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced.", comment: "Text for fetch favicons onboarding dialog") - static let keepFaviconsUpdated = NSLocalizedString("prefrences.sync.keep-favicons-updated", value: "Keep Bookmarks Icons Updated", comment: "Title of the confirmation button for favicons fetching") + static let syncLimitExceededTitle = NSLocalizedString("prefrences.sync.limit-exceeded-title", bundle: Bundle.module, value: "Sync Paused", comment: "Title for sync limits exceeded warning") + static let bookmarksLimitExceededDescription = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-description", bundle: Bundle.module, 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", bundle: Bundle.module, 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", bundle: Bundle.module, value: "Manage Bookmarks", comment: "Button title for sync bookmarks limits exceeded warning to go to manage bookmarks") + static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", bundle: Bundle.module, value: "Manage passwords…", comment: "Button title for sync credentials limits exceeded warning to go to manage passwords") + static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", bundle: Bundle.module, value: "Sync & Backup Error", comment: "Title for sync error alert") + static let unableToSyncToServerDescription = NSLocalizedString("alert.unable-to-sync-to-server-description", bundle: Bundle.module, value: "Unable to connect to the server.", comment: "Description for unable to sync to server error") + static let unableToSyncWithAnotherDeviceDescription = NSLocalizedString("alert.unable-to-sync-with-another-device-description", bundle: Bundle.module, value: "Unable to Sync with another device.", comment: "Description for unable to sync with another device error") + static let unableToMergeTwoAccountsDescription = NSLocalizedString("alert.unable-to-merge-two-accounts-description", bundle: Bundle.module, value: "To pair these devices, turn off Sync & Backup on one device then tap \"Sync With Another Device\" on the other device.", comment: "Description for unable to merge two accounts error") + static let unableToUpdateDeviceNameDescription = NSLocalizedString("alert.unable-to-update-device-name-description", bundle: Bundle.module, value: "Unable to update the device name.", comment: "Description for unable to update device name error") + static let unableToTurnSyncOffDescription = NSLocalizedString("alert.unable-to-turn-sync-off-description", bundle: Bundle.module, value: "Unable to turn Sync & Backup off.", comment: "Description for unable to turn sync off error") + static let unableToDeleteDataDescription = NSLocalizedString("alert.unable-to-delete-data-description", bundle: Bundle.module, value: "Unable to delete data on the server.", comment: "Description for unable to delete data error") + static let unableToRemoveDeviceDescription = NSLocalizedString("alert.unable-to-remove-device-description", bundle: Bundle.module, value: "Unable to remove this device from Sync & Backup.", comment: "Description for unable to remove device error") + static let invalidCodeDescription = NSLocalizedString("alert.invalid-code-description", bundle: Bundle.module, value: "Sorry, this code is invalid. Please make sure it was entered correctly.", comment: "Description for invalid code error") + static let unableCreateRecoveryPdfDescription = NSLocalizedString("alert.unable-to-create-recovery-pdf-description", bundle: Bundle.module, value: "Unable to create the recovery PDF.", comment: "Description for unable to create recovery pdf error") + + static let fetchFaviconsOnboardingTitle = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-title", bundle: Bundle.module, value: "Download Missing Icons?", comment: "Title for fetch favicons onboarding dialog") + static let fetchFaviconsOnboardingMessage = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-message", bundle: Bundle.module, value: "Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced.", comment: "Text for fetch favicons onboarding dialog") + static let keepFaviconsUpdated = NSLocalizedString("prefrences.sync.keep-favicons-updated", bundle: Bundle.module, value: "Keep Bookmarks Icons Updated", comment: "Title of the confirmation button for favicons fetching") // Sync Feature Flags - static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync-unavailable", value: "Sync & Backup is Unavailable", comment: "Title of the warning message that sync and backup are unavailable") - static let syncPausedTitle = NSLocalizedString("sync.warning.sync-paused", value: "Sync & Backup is Paused", comment: "Title of the warning message that Sync & Backup is Paused") - static let syncUnavailableMessage = NSLocalizedString("sync.warning.sync-unavailable-message", value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") - static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data-syncing-disabled-upgrade-required", value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") + static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync-unavailable", bundle: Bundle.module, value: "Sync & Backup is Unavailable", comment: "Title of the warning message that sync and backup are unavailable") + static let syncPausedTitle = NSLocalizedString("sync.warning.sync-paused", bundle: Bundle.module, value: "Sync & Backup is Paused", comment: "Title of the warning message that Sync & Backup is Paused") + static let syncUnavailableMessage = NSLocalizedString("sync.warning.sync-unavailable-message", bundle: Bundle.module, value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") + static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data-syncing-disabled-upgrade-required", bundle: Bundle.module, value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") } From 3dad84b6044b297e113ac6ce7b774a62928e96bb Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 22 Feb 2024 09:46:59 -0300 Subject: [PATCH 09/12] Mute/unmute tab (#2019) --- .../MutedTabIconColor.colorset/Contents.json | 38 ++++++++ .../Contents.json | 38 ++++++++ .../Audio-Mute.imageset/Audio-Mute-12.pdf | Bin 0 -> 4374 bytes .../Images/Audio-Mute.imageset/Contents.json | 12 +++ .../Images/Audio.imageset/Audio-12.pdf | Bin 0 -> 4307 bytes .../Images/Audio.imageset/Contents.json | 12 +++ .../Extensions/WKWebViewExtension.swift | 48 ++++++++++ DuckDuckGo/Common/Localizables/UserText.swift | 2 + DuckDuckGo/Localizable.xcstrings | 24 +++++ .../Model/PinnedTabsViewModel.swift | 27 ++++++ .../PinnedTabs/View/PinnedTabView.swift | 50 +++++++++-- DuckDuckGo/Tab/Model/Tab.swift | 9 ++ .../TabBar/View/Base.lproj/TabBar.storyboard | 21 +++-- .../TabBar/View/TabBarViewController.swift | 23 +++++ DuckDuckGo/TabBar/View/TabBarViewItem.swift | 83 ++++++++++++++++-- DuckDuckGo/TabBar/View/TabBarViewItem.xib | 41 ++++++--- .../PinnedTabs/PinnedTabsViewModelTests.swift | 6 +- .../TabBar/View/MockTabViewItemDelegate.swift | 13 +++ .../TabBar/View/TabBarViewItemTests.swift | 22 +++++ 19 files changed, 427 insertions(+), 42 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json new file mode 100644 index 0000000000..3fe9b59242 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json new file mode 100644 index 0000000000..802fa68a4c --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf new file mode 100644 index 0000000000000000000000000000000000000000..de1f4718ed0a91177028ae95b498623cae76a1cb GIT binary patch literal 4374 zcmeH~TaOb*5QX38SM&=KJZ!t~7YQM8SCk;gZXS?O9*lQ@CA&7UO$7P%`Kmph@m>-Z zo-;!1_)J%I^{G?U-FIF+d3I!V?mDZCTD|?)DRuw8n*H|mQ?F*PU%vg=FScMjvtRn< z`|Sti7e44NO<>Kq5?++dfu1-F`-L^@et=1O< zcHD1PpVy0iBU6kcA3a)qQE!ydrajY(b$e)f`yE$UZno=we$maI^naZ#`s3%1)Z>%G zUyDumXJ_~3qwAyb?fSQ%LpI3-wYccUVb@RnRsX=Zw1OtkEH);;i!8 z)*4e*=K?0i*{GaODdtoaP>#m)#`0?QIS!EZPFWpY4z8$$nC4JRB$2v=l1r9x1ZblN zvbmVjOuch5inRq_qcuBQ85f;G3?^pFvZ_wr+MESSCObY>ZEf|bs3Ss&t{RVR(Ir+B zl7KGPn1cfHHnSk0l!H$eQxi+@&9r*f4R2l55(yC;2?f!ZB(vsPDyCjL9zw3VhQ=A* zgeNPrvb0!Iwr$$rT`EaIvo+C_wu{Ib^PDPPux%;}%;&T(!3HCy#bz=~DxJ5XHg6%hkS!@Uux46{2qsuxjDQi2 zTFk-LY-Jp*2j|7xRkbX-58b8G(0#vY@A{#`+ zxEwuFYk^cI6;mk(*J3ltmufLZ^DrT^dI|zmwCpHi!UQ7i7V!WDYLf{Zgc}yoOaY6C z%t07p$TH5?6r(_a5`vu@X#eqaI~cSj-li#~eYeZ#zK|-iJz`N)p}8iK9i;t88cCSH zie;qAVr3kqj((ATWR20og<$>%tB(MGWmquN|DxDTnW-+ z9nPeL=<_LhLkBk6dpfMcm%&$}RZ83N)Uq^NiHhi@A7!L{hzv1^uVGZ6<+WJF*MLP$ zcMABYsXK&PRMMXHu%eNh5s@`kV|}dCD9Ib7me!54p|42c22HU@pQ^-PE&Ilpae8*i zkpdo(Q5QchN9xYA6VW!mxkrxEQn!RCIk}j$ca*5$1|A3!Fts+{R+a)g12vg+OlVS; z>opC4?x&8VcX;}28#(X-SlKAk0Wnm0(TQ=5${`m(HWmI&)L`mZ7Oi42)yx$5`46{i zbj5Z>Q~Wkzvbh&ebCeB(mR8VNJ5^LiOJ=7>(w_a&wl{XtbOl4TRpdQdqa)eKXo0AV zLqxP}=kk5RLL|DjYwfL(KieDiA}VBe6PL)iBzAGwrUYu1sb!;$*)R~###v9BJ%FV4 zAbqry`ta6Q+O5=@HFWd?BvX>a#vx#D$5tZ@5XB9=ObO}w<>_!3tQ zt>7xeetH@5o?FXRC_tG!qc5MX8|LeyIJ;jptR4<8`3jtNMrga9pPl#XPU!vV3FO}0 wtSh9U;y&c7~J1!o9%qPZR`Y-eRt>1)0fZw0h>RF!2kdN literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json new file mode 100644 index 0000000000..b609317961 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Audio-Mute-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf new file mode 100644 index 0000000000000000000000000000000000000000..635e45f8743e3360c3aa3f3fe264526a6d890242 GIT binary patch literal 4307 zcmeH~O>Y!O5QgvbEBb;2k(ll84+&WU8>}cn5IYAXl*6(fj1zm;*ysCcO`Dp=D6LlSes7d|@f`O6LH)abS8aO3bNsF6%gyb4-7OcN@4P=c8eE-!e7CKezF4hq z2JEcctUj(6-A1+;S3Y~T`lQ|}-RkX z{l6BQ=Fi6L%tzOc$ESPW1>>X1YH`zy>kc0_i)G_mTXardNnlYYW7G{`v1E`1jK%rj zRoq1YWPK{e?m?z?Aw*l$qM6pUF}diHx}GrIrfgiWYSH7|+Y)mK0biyq z+PJ8dYh5xaMSPhy+CXgk7;bF}E>GB)t8aBFj$rqYQesNtf5dXDo%O}we-A6R6fz&Z zmo|nZtHu~cWEX2FYQ(PZ@k(LmjVNGt_dhht=9lkQQEa^r24=r5qSRXJs2)5(q^RkpNtnkH1JPS+3%W{e_Qp{Hkl;%Y z<_$%!b=A&z=Shs3MQ1gFq^o0ykyQyf2dR&fKFA=&7wa9piBORUcBM5wX*8%?-sK$V zM0%P|klweb$Mi^XPBA@XUqTQ=s%aq*uo#!?I^(m2X9loC>r4{13`W~1P}VV8h6}MG zyHv7zOiGUAWSx(6GFwnJBC8!|bc!(vR3Nm@2_b_x*}@Sx@fP^;K}&T7GT25S3kUeX zH2X|%v1?)!*(6!mHsp{~Vx4y02BM?Kit)OdmLw`8U>Z00R84~+ew81*G9pTO0$;2&fHj3%TOoNDc^@IfjH-#cyOdlXW#MB?%|GHKEcZ z-rz|FprP9AAe(v-C?k>HFufuCnItu3^b{{W6bw(YDl>z#AeQAsLs$tK`&enGl4Mdj zDQ#v}4{@PO<}0E)?Ae8A-XFfjknUcaaJsGc(?#)4dLW(x{n6 z5yUZKP*&mVWKoaUnM{4)HMg0MNvO&SIYI_Vv{pN^Lq$1R>?n}KH~G?yStQ1O&L!F= zwuB&+WD?V}Gy)KOd1?y8H^E~Nm}xffj%i3Cbg=jh)ZWw8b%@ip^%LftD<0+24ph)t zK1eYHhnTA=13MEfD!?#0S%*|ikcog83(Zw`wX%vrWjGOO1IjX#AvM;irWKuG(9#FN z!i<+h4Ly}XWWFbdA#^~eM;6j}hV+FYI!eTdrH{VO2_3RWyFf@g(=K%?5QV84&@mZ? z6ttmrnp_k>p718s)DT`x`Kr=aHC(EZOx>2`GhU2g)CHa8L+q$9bO9kZ@xnVM7ox$I z=|Y3!M@Ff3a05i|O1UiMN^&&l4^caUeae-kdd6Z%@DU1zu}E#iTQZEfzzGHY)<96Z zM;j64l~fyob&M!_L3Dzpd<&tc)UPxH0`95EVe3wcX}aabaFXLmPOFWaM*CwcV>rly z3M1WbJI!+P`2RG^{HCAl?;D28xU_E&%$P3I^Yp|SW;~IPMtg_9`>@HP9xCdfyr=9^ zwxR6Kj#cL)yjS(%_EN7WZ;-lFdb&kC|Fk_l-_EyPegE9l|IS~|H-FTp{sJU_-np(! zx0<_8vr{E4H{6TvzB~A9#)atV_3elG*In-}JqJDn7;b4}CjaK{=%`N{XFGkwJ4_8X zJLX)^&^O(dW^i`7X=YG+vf|`-PnE=BUnL*Mt(l#BMS8kiuC^QXR_-w&o!I4alA+#`C@_$#667IAwl3-_EaBmv?PHt-JS)Ox30rsr;+tn->)L@RJSX zr)qW|P7ZWmLWvaRH~Mnn&@djZiUq>%8p`qD3y7282yNH%tLtvv2)#d^ zK<>lM>ZW_79$sBM>M5R`tyb_W^woOJ&aeLNgbT>sZ0GB3WyfnBn+Fe`zk2Zx7O#DG literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json new file mode 100644 index 0000000000..35d4dda319 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Audio-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index a2eb4706e4..4c70d4bacc 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -29,6 +29,12 @@ extension WKWebView { return false } + enum AudioState { + case muted + case unmuted + case notSupported + } + enum CaptureState { case none case active @@ -129,6 +135,48 @@ extension WKWebView { } #endif + func muteOrUnmute() { +#if !APPSTORE + guard self.responds(to: #selector(WKWebView._setPageMuted(_:))) else { + assertionFailure("WKWebView does not respond to selector _stopMediaCapture") + return + } + let mutedState: _WKMediaMutedState = { + guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { return [] } + return self._mediaMutedState() + }() + var newState = mutedState + + if newState == .audioMuted { + newState.remove(.audioMuted) + } else { + newState.insert(.audioMuted) + } + guard newState != mutedState else { return } + self._setPageMuted(newState) +#endif + } + + /// Returns the audio state of the WKWebView. + /// + /// - Returns: `muted` if the web view is muted + /// `unmuted` if the web view is unmuted + /// `notSupported` if the web view does not support fetching the current audio state + func audioState() -> AudioState { +#if APPSTORE + return .notSupported +#else + guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { + assertionFailure("WKWebView does not respond to selector _mediaMutedState") + return .notSupported + } + + let mutedState = self._mediaMutedState() + + return mutedState.contains(.audioMuted) ? .muted : .unmuted +#endif + } + func stopMediaCapture() { guard #available(macOS 12.0, *) else { #if !APPSTORE diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index c3f8ff0f68..eee5b3f4e5 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -199,6 +199,8 @@ struct UserText { static let pinTab = NSLocalizedString("pin.tab", value: "Pin Tab", comment: "Menu item. Pin as a verb") static let unpinTab = NSLocalizedString("unpin.tab", value: "Unpin Tab", comment: "Menu item. Unpin as a verb") static let closeTab = NSLocalizedString("close.tab", value: "Close Tab", comment: "Menu item") + static let muteTab = NSLocalizedString("mute.tab", value: "Mute Tab", comment: "Menu item. Mute tab") + static let unmuteTab = NSLocalizedString("unmute.tab", value: "Unmute Tab", comment: "Menu item. Unmute tab") static let closeOtherTabs = NSLocalizedString("close.other.tabs", value: "Close Other Tabs", comment: "Menu item") static let closeTabsToTheRight = NSLocalizedString("close.tabs.to.the.right", value: "Close Tabs to the Right", comment: "Menu item") static let openInNewTab = NSLocalizedString("open.in.new.tab", value: "Open in New Tab", comment: "Menu item that opens the link in a new tab") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index faef1b6820..ddf6e62331 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -5092,6 +5092,18 @@ } } }, + "mute.tab" : { + "comment" : "Menu item. Mute tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Mute Tab" + } + } + } + }, "n.more.tabs" : { "comment" : "String in Recently Closed menu item for recently closed browser window and number of tabs contained in the closed window", "extractionState" : "extracted_with_value", @@ -9100,6 +9112,18 @@ } } }, + "unmute.tab" : { + "comment" : "Menu item. Unmute tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unmute Tab" + } + } + } + }, "unpin.tab" : { "comment" : "Menu item. Unpin as a verb", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift index 2c714304df..ee5c9f0c91 100644 --- a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift +++ b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift @@ -46,6 +46,7 @@ final class PinnedTabsViewModel: ObservableObject { didSet { if let selectedItem = selectedItem { selectedItemIndex = items.firstIndex(of: selectedItem) + updateTabAudioState(tab: selectedItem) } else { selectedItemIndex = nil } @@ -57,6 +58,7 @@ final class PinnedTabsViewModel: ObservableObject { didSet { if let hoveredItem = hoveredItem { hoveredItemIndex = items.firstIndex(of: hoveredItem) + updateTabAudioState(tab: hoveredItem) } else { hoveredItemIndex = nil } @@ -72,6 +74,7 @@ final class PinnedTabsViewModel: ObservableObject { @Published private(set) var selectedItemIndex: Int? @Published private(set) var hoveredItemIndex: Int? @Published private(set) var dragMovesWindow: Bool = true + @Published private(set) var audioStateView: AudioStateView = .notSupported @Published private(set) var itemsWithoutSeparator: Set = [] @@ -111,6 +114,18 @@ final class PinnedTabsViewModel: ObservableObject { } itemsWithoutSeparator = items } + + private func updateTabAudioState(tab: Tab) { + let audioState = tab.audioState + switch audioState { + case .muted: + audioStateView = .muted + case .unmuted: + audioStateView = .unmuted + case .notSupported: + audioStateView = .notSupported + } + } } // MARK: - Context Menu @@ -124,6 +139,13 @@ extension PinnedTabsViewModel { case fireproof(Tab) case removeFireproofing(Tab) case close(Int) + case muteOrUnmute(Tab) + } + + enum AudioStateView { + case muted + case unmuted + case notSupported } func isFireproof(_ tab: Tab) -> Bool { @@ -168,4 +190,9 @@ extension PinnedTabsViewModel { func removeFireproofing(_ tab: Tab) { contextMenuActionSubject.send(.removeFireproofing(tab)) } + + func muteOrUmute(_ tab: Tab) { + contextMenuActionSubject.send(.muteOrUnmute(tab)) + updateTabAudioState(tab: tab) + } } diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 148b0d5d82..278fdfa7ca 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -21,8 +21,8 @@ import SwiftUIExtensions struct PinnedTabView: View { enum Const { - static let dimension: CGFloat = 32 - static let cornerRadius: CGFloat = 6 + static let dimension: CGFloat = 34 + static let cornerRadius: CGFloat = 10 } @ObservedObject var model: Tab @@ -96,7 +96,17 @@ struct PinnedTabView: View { fireproofAction Divider() - + switch collectionModel.audioStateView { + case .muted, .unmuted: + let audioStateText = collectionModel.audioStateView == .muted ? UserText.unmuteTab : UserText.muteTab + Button(audioStateText) { [weak collectionModel, weak model] in + guard let model = model else { return } + collectionModel?.muteOrUmute(model) + } + Divider() + case .notSupported: + EmptyView() + } Button(UserText.closeTab) { [weak collectionModel, weak model] in guard let model = model else { return } collectionModel?.close(model) @@ -163,6 +173,7 @@ struct PinnedTabInnerView: View { var foregroundColor: Color var drawSeparator: Bool = true + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var model: Tab @Environment(\.controlActiveState) private var controlActiveState @@ -187,11 +198,32 @@ struct PinnedTabInnerView: View { .frame(width: PinnedTabView.Const.dimension) } + @ViewBuilder + var mutedTabIndicator: some View { + switch model.audioState { + case .muted: + ZStack { + Circle() + .stroke(Color.gray.opacity(0.5), lineWidth: 0.5) + .background(Circle().foregroundColor(Color("PinnedTabMuteStateCircleColor"))) + .frame(width: 16, height: 16) + Image("Audio-Mute") + .resizable() + .renderingMode(.template) + .frame(width: 12, height: 12) + }.offset(x: 8, y: -8) + default: EmptyView() + } + } + @ViewBuilder var favicon: some View { if let favicon = model.favicon { - Image(nsImage: favicon) - .resizable() + ZStack(alignment: .topTrailing) { + Image(nsImage: favicon) + .resizable() + mutedTabIndicator + } } else if let domain = model.content.url?.host, let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain), let firstLetter = eTLDplus1.capitalized.first.flatMap(String.init) { ZStack { Rectangle() @@ -199,11 +231,15 @@ struct PinnedTabInnerView: View { Text(firstLetter) .font(.caption) .foregroundColor(.white) + mutedTabIndicator } .cornerRadius(4.0) } else { - Image(nsImage: #imageLiteral(resourceName: "Web")) - .resizable() + ZStack { + Image(nsImage: #imageLiteral(resourceName: "Web")) + .resizable() + mutedTabIndicator + } } } } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index aba8f89859..5c1c11d1db 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -488,6 +488,7 @@ protocol NewWindowPolicyDecisionMaker { } #endif + self.audioState = webView.audioState() addDeallocationChecks(for: webView) } @@ -934,6 +935,14 @@ protocol NewWindowPolicyDecisionMaker { } } + @Published private(set) var audioState: WKWebView.AudioState = .notSupported + + func muteUnmuteTab() { + webView.muteOrUnmute() + + audioState = webView.audioState() + } + @MainActor(unsafe) @discardableResult private func reloadIfNeeded(shouldLoadInBackground: Bool = false) -> ExpectedNavigation? { diff --git a/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard b/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard index 2bf37ba9b2..ff559dfdfe 100644 --- a/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard +++ b/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard @@ -1,7 +1,7 @@ - + - + @@ -62,7 +62,7 @@ - + - + @@ -116,7 +125,7 @@ - + @@ -124,7 +133,9 @@ + + @@ -134,11 +145,12 @@ - - + + + @@ -147,7 +159,7 @@ - + @@ -158,17 +170,20 @@ + + + diff --git a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift index 6e34010422..c9f063d54f 100644 --- a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift +++ b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift @@ -149,17 +149,19 @@ class PinnedTabsViewModelTests: XCTestCase { model.fireproof(tabA) model.removeFireproofing(tabB) model.close(tabA) + model.muteOrUmute(tabB) cancellable.cancel() - XCTAssertEqual(events.count, 6) + XCTAssertEqual(events.count, 7) guard case .bookmark(tabA) = events[0], case .unpin(1) = events[1], case .duplicate(0) = events[2], case .fireproof(tabA) = events[3], case .removeFireproofing(tabB) = events[4], - case .close(0) = events[5] + case .close(0) = events[5], + case .muteOrUnmute(tabB) = events[6] else { XCTFail("Incorrect context menu action") return diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index 9d576f6550..ab577650a2 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -22,6 +22,7 @@ import Foundation class MockTabViewItemDelegate: TabBarViewItemDelegate { var hasItemsToTheRight = false + var audioState: WKWebView.AudioState = .notSupported func tabBarViewItem(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem, isMouseOver: Bool) { @@ -75,8 +76,20 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } + func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState { + return audioState + } + + func tabBarViewItemMuteUnmuteSite(_ tabBarViewItem: TabBarViewItem) { + + } + func otherTabBarViewItemsState(for tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> DuckDuckGo_Privacy_Browser.OtherTabBarViewItemsState { OtherTabBarViewItemsState(hasItemsToTheLeft: false, hasItemsToTheRight: hasItemsToTheRight) } + func clear() { + self.audioState = .notSupported + } + } diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index d855470109..c4e8f4418b 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -33,6 +33,10 @@ final class TabBarViewItemTests: XCTestCase { tabBarViewItem.delegate = delegate } + override func tearDown() { + delegate.clear() + } + func testThatAllExpectedItemsAreShown() { tabBarViewItem.menuNeedsUpdate(menu) @@ -48,6 +52,24 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertEqual(menu.item(at: 9)?.title, UserText.moveTabToNewWindow) } + func testThatMuteIsShownWhenCurrentAudioStateIsUnmuted() { + delegate.audioState = .unmuted + tabBarViewItem.menuNeedsUpdate(menu) + + XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.muteTab) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + } + + func testThatUnmuteIsShownWhenCurrentAudioStateIsMuted() { + delegate.audioState = .muted + tabBarViewItem.menuNeedsUpdate(menu) + + XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.unmuteTab) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + } + func testWhenOneTabCloseThenOtherTabsItemIsDisabled() { tabBarViewItem.menuNeedsUpdate(menu) From 469dd5bc110d5955933660f657e1620d4d7ed4cb Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 22 Feb 2024 18:21:01 +0000 Subject: [PATCH 10/12] Add identifier model to ExtractedProfile (#2188) Task/Issue URL: https://app.asana.com/0/1204167627774280/1205703170200803/f **Description**: Add identifier model to ExtractedProfile --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++--- .../DataBrokerProtection/Package.swift | 2 +- .../Model/ExtractedProfile.swift | 31 +++++++++++++++++-- ...taBrokerProfileQueryOperationManager.swift | 6 ++-- .../JSON/advancedbackgroundchecks.com.json | 16 +++++----- .../Resources/JSON/centeda.com.json | 12 +++---- .../JSON/freepeopledirectory.com.json | 10 ++++-- .../Storage/Mappers.swift | 3 +- LocalPackages/LoginItems/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/PixelKit/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- LocalPackages/SwiftUIExtensions/Package.swift | 2 +- LocalPackages/SyncUI/Package.swift | 2 +- .../SystemExtensionManager/Package.swift | 2 +- LocalPackages/XPCHelper/Package.swift | 2 +- 17 files changed, 70 insertions(+), 36 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0a586a3376..68632bf6e3 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13538,7 +13538,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 109.0.1; + version = 109.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 e0a1c42442..dbe440a602 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" : "da6a822844922401d80e26963b8b11dcd6ef221a", - "version" : "109.0.1" + "revision" : "da5f8ae73e7ad7fc47931f82f5ac6c4fafa6ac94", + "version" : "109.0.2" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "063b560e59a50e03d9b00b88a7fcb2ed2b562395", - "version" : "4.61.0" + "revision" : "36ddba2cbac52a41b9a9275af06d32fa8a56d2d7", + "version" : "4.64.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 64fa721012..e8853bdc23 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: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift index 29757bc891..1fd0366c8e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift @@ -77,6 +77,7 @@ struct ExtractedProfile: Codable, Sendable { var email: String? var removedDate: Date? let fullName: String? + let identifier: String? enum CodingKeys: CodingKey { case id @@ -92,6 +93,7 @@ struct ExtractedProfile: Codable, Sendable { case email case removedDate case fullName + case identifier } init(id: Int64? = nil, @@ -105,7 +107,8 @@ struct ExtractedProfile: Codable, Sendable { reportId: String? = nil, age: String? = nil, email: String? = nil, - removedDate: Date? = nil) { + removedDate: Date? = nil, + identifier: String? = nil) { self.id = id self.name = name self.alternativeNames = alternativeNames @@ -119,6 +122,29 @@ struct ExtractedProfile: Codable, Sendable { self.email = email self.removedDate = removedDate self.fullName = name + self.identifier = identifier + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decodeIfPresent(Int64.self, forKey: .id) + name = try container.decodeIfPresent(String.self, forKey: .name) + alternativeNames = try container.decodeIfPresent([String].self, forKey: .alternativeNames) + addressFull = try container.decodeIfPresent(String.self, forKey: .addressFull) + addresses = try container.decodeIfPresent([AddressCityState].self, forKey: .addresses) + phoneNumbers = try container.decodeIfPresent([String].self, forKey: .phoneNumbers) + relatives = try container.decodeIfPresent([String].self, forKey: .relatives) + profileUrl = try container.decode(String.self, forKey: .profileUrl) + reportId = try container.decodeIfPresent(String.self, forKey: .reportId) + age = try container.decodeIfPresent(String.self, forKey: .age) + email = try container.decodeIfPresent(String.self, forKey: .email) + removedDate = try container.decodeIfPresent(Date.self, forKey: .removedDate) + fullName = try container.decodeIfPresent(String.self, forKey: .fullName) + if let identifier = try container.decodeIfPresent(String.self, forKey: .identifier) { + self.identifier = identifier + } else { + self.identifier = profileUrl + } } func merge(with profile: ProfileQuery) -> ExtractedProfile { @@ -134,7 +160,8 @@ struct ExtractedProfile: Codable, Sendable { reportId: self.reportId, age: self.age ?? String(profile.age), email: self.email, - removedDate: self.removedDate + removedDate: self.removedDate, + identifier: self.identifier ) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index 1b043a6d41..73f08604f6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -135,10 +135,10 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { // We check if the profile exists in the database. let extractedProfilesForBroker = database.fetchExtractedProfiles(for: brokerId) - let doesProfileExistsInDatabase = extractedProfilesForBroker.contains { $0.profileUrl == extractedProfile.profileUrl } + let doesProfileExistsInDatabase = extractedProfilesForBroker.contains { $0.identifier == extractedProfile.identifier } // If the profile exists we do not create a new opt-out operation - if doesProfileExistsInDatabase, let alreadyInDatabaseProfile = extractedProfilesForBroker.first(where: { $0.profileUrl == extractedProfile.profileUrl }), let id = alreadyInDatabaseProfile.id { + if doesProfileExistsInDatabase, let alreadyInDatabaseProfile = extractedProfilesForBroker.first(where: { $0.identifier == extractedProfile.identifier }), let id = alreadyInDatabaseProfile.id { // If it was removed in the past but was found again when scanning, it means it appearead again, so we reset the remove date. if alreadyInDatabaseProfile.removedDate != nil { database.updateRemovedDate(nil, on: id) @@ -176,7 +176,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { // Check for removed profiles let removedProfiles = brokerProfileQueryData.extractedProfiles.filter { savedProfile in !extractedProfiles.contains { recentlyFoundProfile in - recentlyFoundProfile.profileUrl == savedProfile.profileUrl + recentlyFoundProfile.identifier == savedProfile.identifier } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json index ee35af6d17..d7daac5036 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json @@ -1,9 +1,9 @@ { "name": "AdvancedBackgroundChecks", "url": "advancedbackgroundchecks.com", - "version": "0.1.4", + "version": "0.1.5", "parent": "peoplefinders.com", - "addedDatetime": 1678082400000, + "addedDatetime": 1678060800000, "steps": [ { "stepType": "scan", @@ -11,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "c73ba931-9e01-4d37-9e15-2fd7a14eefa3", + "id": "7967f064-e3c5-442d-8380-99cf752fb8df", "url": "https://www.advancedbackgroundchecks.com/names/${firstName}-${lastName}_${city}-${state}_age_${age}" }, { "actionType": "extract", - "id": "94003082-0a9d-4418-ac88-68595c7f4953", + "id": "6f6bb616-a4cb-4231-9abb-522722208f95", "selector": ".card-block", "profile": { "name": { @@ -25,7 +25,8 @@ }, "alternativeNamesList": { "selector": "(.//p[@class='card-text max-lines-1'])[1]", - "afterText": "AKA:" + "afterText": "AKA:", + "separator": "," }, "age": { "selector": ".card-title", @@ -40,7 +41,8 @@ }, "relativesList": { "selector": "(.//p[@class='card-text max-lines-1'])[2]", - "afterText": "Related to:" + "afterText": "Related to:", + "separator": "," }, "profileUrl": { "selector": ".link-to-details" @@ -60,4 +62,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} \ No newline at end of file +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json index bb15f0093f..7050bab137 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json @@ -3,7 +3,7 @@ "url": "centeda.com", "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677736800000, + "addedDatetime": 1677715200000, "steps": [ { "stepType": "scan", @@ -11,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "2f6639c0-201f-4d5e-8467-ae0ba457b409", + "id": "af9c9f03-e778-4c29-85fc-e5cbbfec563c", "url": "https://centeda.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -25,8 +25,8 @@ }, { "actionType": "extract", - "id": "e2e236b0-515b-43b3-9154-0432ed9b7566", - "selector": ".search-item", + "id": "79fa2a1c-65b4-417a-a8ac-2ca6d729ffc1", + "selector": ".search-result > a", "profile": { "name": { "selector": ".title", @@ -47,7 +47,7 @@ "selector": ".//div[@class='col-sm-24 col-md-8 related-to']//li" }, "profileUrl": { - "selector": ".get-report-btn" + "selector": "a" } } } @@ -64,4 +64,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} \ No newline at end of file +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json index c448989448..9d114d48b3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json @@ -11,17 +11,21 @@ "actions": [ { "actionType": "navigate", - "id": "4c607417-36bc-47d4-8562-9c2244db354d", + "id": "b8b912b0-201d-4cd1-8237-235c34fe0fea", "url": "https://www.freepeopledirectory.com/name/${firstName}-${lastName}/${state|upcase}/${city}" }, { "actionType": "extract", - "id": "a1637310-ca7a-40b0-b2f5-db22b43b5d54", + "id": "50e30922-ef1d-4820-abbd-f536378472d4", "selector": ".whole-card", "profile": { "name": { "selector": ".card-title" }, + "alternativeNamesList": { + "selector": ".//h3/span[contains(text(),'AKA:')]/following-sibling::span", + "afterText": "No other aliases." + }, "addressCityState": { "selector": ".city" }, @@ -50,4 +54,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} \ No newline at end of file +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift index 56b08dd5f2..81cf02f70d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift @@ -233,7 +233,8 @@ struct MapperToModel { reportId: extractedProfile.reportId, age: extractedProfile.age, email: extractedProfile.email, - removedDate: extractedProfileDB.removedDate) + removedDate: extractedProfileDB.removedDate, + identifier: extractedProfile.identifier) } func mapToModel(_ scanEvent: ScanHistoryEventDB) throws -> HistoryEvent { diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index a631a199d9..b7b7bb72d9 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -13,7 +13,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index e2dc908671..f1e2140732 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems") diff --git a/LocalPackages/PixelKit/Package.swift b/LocalPackages/PixelKit/Package.swift index 1222931baa..0cc6651ae2 100644 --- a/LocalPackages/PixelKit/Package.swift +++ b/LocalPackages/PixelKit/Package.swift @@ -20,7 +20,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index e5192fc149..b7af9e0bbb 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SwiftUIExtensions/Package.swift b/LocalPackages/SwiftUIExtensions/Package.swift index 10d667a750..249d2a6cbb 100644 --- a/LocalPackages/SwiftUIExtensions/Package.swift +++ b/LocalPackages/SwiftUIExtensions/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library(name: "PreferencesViews", targets: ["PreferencesViews"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index b927525124..8928521a9e 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(path: "../SwiftUIExtensions"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 43bafef378..0ae14903d5 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -16,7 +16,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/XPCHelper/Package.swift b/LocalPackages/XPCHelper/Package.swift index e62141ec7a..323f247b91 100644 --- a/LocalPackages/XPCHelper/Package.swift +++ b/LocalPackages/XPCHelper/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "XPCHelper", targets: ["XPCHelper"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( From 40290b0aeebb10c3a216fe66edba3480a83b93ed Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 22 Feb 2024 19:28:44 +0000 Subject: [PATCH 11/12] Fix DBP decoding tests (#2244) Task/Issue URL:https://app.asana.com/0/1199230911884351/1206672196780651/f **Description**: Fix tests after the introduction of a new property --- .../Model/ExtractedProfile.swift | 2 +- ...DataBrokerProfileQueryOperationManagerTests.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift index 1fd0366c8e..70a76305b3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift @@ -134,7 +134,7 @@ struct ExtractedProfile: Codable, Sendable { addresses = try container.decodeIfPresent([AddressCityState].self, forKey: .addresses) phoneNumbers = try container.decodeIfPresent([String].self, forKey: .phoneNumbers) relatives = try container.decodeIfPresent([String].self, forKey: .relatives) - profileUrl = try container.decode(String.self, forKey: .profileUrl) + profileUrl = try container.decodeIfPresent(String.self, forKey: .profileUrl) reportId = try container.decodeIfPresent(String.self, forKey: .reportId) age = try container.decodeIfPresent(String.self, forKey: .age) email = try container.decodeIfPresent(String.self, forKey: .email) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index 5fa7f4b069..ca2ea7a3c0 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -98,8 +98,8 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] let mockScanOperation = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) - let extractedProfileSaved1 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "abc") - let extractedProfileSaved2 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "zxz") + let extractedProfileSaved1 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "abc", identifier: "abc") + let extractedProfileSaved2 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "zxz", identifier: "zxz") let optOutData = [OptOutOperationData.mock(with: extractedProfileSaved1), OptOutOperationData.mock(with: extractedProfileSaved2)] @@ -907,19 +907,19 @@ extension ProfileQuery { extension ExtractedProfile { static var mockWithRemovedDate: ExtractedProfile { - ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL", removedDate: Date()) + ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL", removedDate: Date(), identifier: "someURL") } static var mockWithoutRemovedDate: ExtractedProfile { - ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL") + ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL", identifier: "someURL") } static var mockWithoutId: ExtractedProfile { - ExtractedProfile(name: "Some name", profileUrl: "someOtherURL") + ExtractedProfile(name: "Some name", profileUrl: "someOtherURL", identifier: "someOtherURL") } static func mockWithRemoveDate(_ date: Date) -> ExtractedProfile { - ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL", removedDate: date) + ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL", removedDate: date, identifier: "someURL") } } From c57419328e0b0678fa4bfc729b5cbd99a9c7abf1 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 22 Feb 2024 16:57:17 -0300 Subject: [PATCH 12/12] DBP: Add engagement Pixels (#2238) --- DuckDuckGo/DBP/DBPHomeViewController.swift | 9 +- ...DataBrokerProtectionEngagementPixels.swift | 174 ++++++++++++++ .../Pixels/DataBrokerProtectionPixels.swift | 184 ++------------- ...kerProtectionStageDurationCalculator.swift | 184 +++++++++++++++ .../DataBrokerProtectionProcessor.swift | 5 + ...rokerProtectionEngagementPixelsTests.swift | 220 ++++++++++++++++++ .../MismatchCalculatorUseCaseTests.swift | 10 +- .../DataBrokerProtectionTests/Mocks.swift | 14 +- 8 files changed, 623 insertions(+), 177 deletions(-) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStageDurationCalculator.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEngagementPixelsTests.swift diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index ab0e4f783b..4ffcdc68c8 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -49,8 +49,8 @@ final class DBPHomeViewController: NSViewController { let privacySettings = PrivacySecurityPreferences.shared let sessionKey = UUID().uuidString let prefs = ContentScopeProperties(gpcEnabled: privacySettings.gpcEnabled, - sessionKey: sessionKey, - featureToggles: features) + sessionKey: sessionKey, + featureToggles: features) return DataBrokerProtectionViewController( scheduler: dataBrokerProtectionManager.scheduler, @@ -191,7 +191,10 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Date? + func getLatestWeeklyPixel() -> Date? + func getLatestMonthlyPixel() -> Date? +} + +final class DataBrokerProtectionEngagementPixelsUserDefaults: DataBrokerProtectionEngagementPixelsRepository { + + enum Consts { + static let dailyPixelKey = "macos.browser.data-broker-protection.dailyPixelKey" + static let weeklyPixelKey = "macos.browser.data-broker-protection.weeklyPixelKey" + static let monthlyPixelKey = "macos.browser.data-broker-protection.monthlyPixelKey" + } + + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + func markDailyPixelSent() { + userDefaults.set(Date(), forKey: Consts.dailyPixelKey) + } + + func markWeeklyPixelSent() { + userDefaults.set(Date(), forKey: Consts.weeklyPixelKey) + } + + func markMonthlyPixelSent() { + userDefaults.set(Date(), forKey: Consts.monthlyPixelKey) + } + + func getLatestDailyPixel() -> Date? { + userDefaults.object(forKey: Consts.dailyPixelKey) as? Date + } + + func getLatestWeeklyPixel() -> Date? { + userDefaults.object(forKey: Consts.weeklyPixelKey) as? Date + } + + func getLatestMonthlyPixel() -> Date? { + userDefaults.object(forKey: Consts.monthlyPixelKey) as? Date + } + +} + +/* + https://app.asana.com/0/1204586965688315/1206648312655381/f + + 1. When a user becomes an "Active User" of your feature, immediately fire individual pixels to register a DAU, a WAU and a MAU. Record (on-device) the date that the pixel was fired for each of the three events. e.g. + - DAU Pixel Last Sent 2024-02-20 + - WAU Pixel Last Sent 2024-02-20 + - MAU Pixel Last Sent 2024-02-20 + 2. If it is >= 1 date since the DAU pixel was last sent, send a new DAU pixel, and update the date with the current date + - DAU Pixel Last Sent 2024-02-21 + - WAU Pixel Last Sent 2024-02-20 + - MAU Pixel Last Sent 2024-02-20 + 3. If it is >= 7 dates since the WAU pixel was last sent, send a new WAU pixel and update the date with the current date + - DAU Pixel Last Sent 2024-02-27 + - WAU Pixel Last Sent 2024-02-27 + - MAU Pixel Last Sent 2024-02-20 + 4. If it is >= 28 dates since the MAU pixel was last sent, send a new MAU pixel and update the date with the current date: + - DAU Pixel Last Sent 2024-03-19 + - WAU Pixel Last Sent 2024-03-19 + - MAU Pixel Last Sent 2024-03-19 + */ +final class DataBrokerProtectionEngagementPixels { + + enum ActiveUserFrequency: Int { + case daily = 1 + case weekly = 7 + case monthly = 28 + } + + private let calendar = Calendar.current + private let database: DataBrokerProtectionRepository + private let repository: DataBrokerProtectionEngagementPixelsRepository + private let handler: EventMapping + + init(database: DataBrokerProtectionRepository, + handler: EventMapping, + repository: DataBrokerProtectionEngagementPixelsRepository = DataBrokerProtectionEngagementPixelsUserDefaults()) { + self.database = database + self.handler = handler + self.repository = repository + } + + func fireEngagementPixel(currentDate: Date = Date()) { + guard database.fetchProfile() != nil else { + os_log("No profile. We do not fire any pixel because we do not consider it an engaged user.", log: .dataBrokerProtection) + return + } + + if shouldWeFireDailyPixel(date: currentDate) { + handler.fire(.dailyActiveUser) + repository.markDailyPixelSent() + } + + if shouldWeFireWeeklyPixel(date: currentDate) { + handler.fire(.weeklyActiveUser) + repository.markWeeklyPixelSent() + } + + if shouldWeFireMonthlyPixel(date: currentDate) { + handler.fire(.monthlyActiveUser) + repository.markMonthlyPixelSent() + } + } + + private func shouldWeFireDailyPixel(date: Date) -> Bool { + guard let latestPixelFire = repository.getLatestDailyPixel() else { + return true + } + + return shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .daily) + } + + private func shouldWeFireWeeklyPixel(date: Date) -> Bool { + guard let latestPixelFire = repository.getLatestWeeklyPixel() else { + return true + } + + return shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .weekly) + } + + private func shouldWeFireMonthlyPixel(date: Date) -> Bool { + guard let latestPixelFire = repository.getLatestMonthlyPixel() else { + return true + } + + return shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .monthly) + } + + private func shouldWeFirePixel(startDate: Date, endDate: Date, daysDifference: ActiveUserFrequency) -> Bool { + if let differenceBetweenDates = differenceBetweenDates(startDate: startDate, endDate: endDate) { + return differenceBetweenDates >= daysDifference.rawValue + } + + return false + } + + private func differenceBetweenDates(startDate: Date, endDate: Date) -> Int? { + let components = calendar.dateComponents([.day], from: startDate, to: endDate) + + return components.day + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index 9f12bbab0c..a791dd55ee 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -39,168 +39,6 @@ enum ErrorCategory: Equatable { } } -enum Stage: String { - case start - case emailGenerate = "email-generate" - case captchaParse = "captcha-parse" - case captchaSend = "captcha-send" - case captchaSolve = "captcha-solve" - case submit - case emailReceive = "email-receive" - case emailConfirm = "email-confirm" - case validate - case other -} - -protocol StageDurationCalculator { - func durationSinceLastStage() -> Double - func durationSinceStartTime() -> Double - func fireOptOutStart() - func fireOptOutEmailGenerate() - func fireOptOutCaptchaParse() - func fireOptOutCaptchaSend() - func fireOptOutCaptchaSolve() - func fireOptOutSubmit() - func fireOptOutEmailReceive() - func fireOptOutEmailConfirm() - func fireOptOutValidate() - func fireOptOutSubmitSuccess() - func fireOptOutFailure() - func fireScanSuccess(matchesFound: Int) - func fireScanFailed() - func fireScanError(error: Error) - func setStage(_ stage: Stage) -} - -final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator { - let handler: EventMapping - let attemptId: UUID - let dataBroker: String - let startTime: Date - var lastStateTime: Date - var stage: Stage = .other - - init(attemptId: UUID = UUID(), - startTime: Date = Date(), - dataBroker: String, - handler: EventMapping) { - self.attemptId = attemptId - self.startTime = startTime - self.lastStateTime = startTime - self.dataBroker = dataBroker - self.handler = handler - } - - /// Returned in milliseconds - func durationSinceLastStage() -> Double { - let now = Date() - let durationSinceLastStage = now.timeIntervalSince(lastStateTime) * 1000 - self.lastStateTime = now - - return durationSinceLastStage.rounded(.towardZero) - } - - /// Returned in milliseconds - func durationSinceStartTime() -> Double { - let now = Date() - return (now.timeIntervalSince(startTime) * 1000).rounded(.towardZero) - } - - func fireOptOutStart() { - setStage(.start) - handler.fire(.optOutStart(dataBroker: dataBroker, attemptId: attemptId)) - } - - func fireOptOutEmailGenerate() { - handler.fire(.optOutEmailGenerate(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutCaptchaParse() { - handler.fire(.optOutCaptchaParse(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutCaptchaSend() { - handler.fire(.optOutCaptchaSend(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutCaptchaSolve() { - handler.fire(.optOutCaptchaSolve(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutSubmit() { - setStage(.submit) - handler.fire(.optOutSubmit(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutEmailReceive() { - handler.fire(.optOutEmailReceive(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutEmailConfirm() { - handler.fire(.optOutEmailConfirm(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutValidate() { - setStage(.validate) - handler.fire(.optOutValidate(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutSubmitSuccess() { - handler.fire(.optOutSubmitSuccess(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutFailure() { - handler.fire(.optOutFailure(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceStartTime(), stage: stage.rawValue)) - } - - func fireScanSuccess(matchesFound: Int) { - handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1)) - } - - func fireScanFailed() { - handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1)) - } - - func fireScanError(error: Error) { - var errorCategory: ErrorCategory = .unclassified - - if let dataBrokerProtectionError = error as? DataBrokerProtectionError { - switch dataBrokerProtectionError { - case .httpError(let httpCode): - if httpCode < 500 { - errorCategory = .clientError(httpCode: httpCode) - } else { - errorCategory = .serverError(httpCode: httpCode) - } - default: - errorCategory = .validationError - } - } else { - if let nsError = error as NSError? { - if nsError.domain == NSURLErrorDomain { - errorCategory = .networkError - } - } - } - - handler.fire( - .scanError( - dataBroker: dataBroker, - duration: durationSinceStartTime(), - category: errorCategory.toString, - details: error.localizedDescription - ) - ) - } - - // Helper methods to set the stage that is about to run. This help us - // identifying the stage so we can know which one was the one that failed. - - func setStage(_ stage: Stage) { - self.stage = stage - } -} - public enum DataBrokerProtectionPixels { struct Consts { static let dataBrokerParamKey = "data_broker" @@ -276,6 +114,11 @@ public enum DataBrokerProtectionPixels { case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int) case scanFailed(dataBroker: String, duration: Double, tries: Int) case scanError(dataBroker: String, duration: Double, category: String, details: String) + + // KPIs - engagement + case dailyActiveUser + case weeklyActiveUser + case monthlyActiveUser } extension DataBrokerProtectionPixels: PixelKitEvent { @@ -331,7 +174,7 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .disableLoginItem: return "m_mac_dbp_login-item_disable" case .resetLoginItem: return "m_mac_dbp_login-item_reset" - // User Notifications + // User Notifications case .dataBrokerProtectionNotificationSentFirstScanComplete: return "m_mac_dbp_notification_sent_first_scan_complete" case .dataBrokerProtectionNotificationOpenedFirstScanComplete: @@ -348,6 +191,11 @@ extension DataBrokerProtectionPixels: PixelKitEvent { return "m_mac_dbp_notification_sent_all_records_removed" case .dataBrokerProtectionNotificationOpenedAllRecordsRemoved: return "m_mac_dbp_notification_opened_all_records_removed" + + // KPIs - engagement + case .dailyActiveUser: return "m_mac_dbp_engagement_dau" + case .weeklyActiveUser: return "m_mac_dbp_engagement_wau" + case .monthlyActiveUser: return "m_mac_dbp_engagement_mau" } } @@ -410,7 +258,10 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .dataBrokerProtectionNotificationScheduled2WeeksCheckIn, .dataBrokerProtectionNotificationOpened2WeeksCheckIn, .dataBrokerProtectionNotificationSentAllRecordsRemoved, - .dataBrokerProtectionNotificationOpenedAllRecordsRemoved: + .dataBrokerProtectionNotificationOpenedAllRecordsRemoved, + .dailyActiveUser, + .weeklyActiveUser, + .monthlyActiveUser: return [:] case .ipcServerRegister, .ipcServerStartScheduler, @@ -485,7 +336,10 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Double + func durationSinceStartTime() -> Double + func fireOptOutStart() + func fireOptOutEmailGenerate() + func fireOptOutCaptchaParse() + func fireOptOutCaptchaSend() + func fireOptOutCaptchaSolve() + func fireOptOutSubmit() + func fireOptOutEmailReceive() + func fireOptOutEmailConfirm() + func fireOptOutValidate() + func fireOptOutSubmitSuccess() + func fireOptOutFailure() + func fireScanSuccess(matchesFound: Int) + func fireScanFailed() + func fireScanError(error: Error) + func setStage(_ stage: Stage) +} + +final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator { + let handler: EventMapping + let attemptId: UUID + let dataBroker: String + let startTime: Date + var lastStateTime: Date + var stage: Stage = .other + + init(attemptId: UUID = UUID(), + startTime: Date = Date(), + dataBroker: String, + handler: EventMapping) { + self.attemptId = attemptId + self.startTime = startTime + self.lastStateTime = startTime + self.dataBroker = dataBroker + self.handler = handler + } + + /// Returned in milliseconds + func durationSinceLastStage() -> Double { + let now = Date() + let durationSinceLastStage = now.timeIntervalSince(lastStateTime) * 1000 + self.lastStateTime = now + + return durationSinceLastStage.rounded(.towardZero) + } + + /// Returned in milliseconds + func durationSinceStartTime() -> Double { + let now = Date() + return (now.timeIntervalSince(startTime) * 1000).rounded(.towardZero) + } + + func fireOptOutStart() { + setStage(.start) + handler.fire(.optOutStart(dataBroker: dataBroker, attemptId: attemptId)) + } + + func fireOptOutEmailGenerate() { + handler.fire(.optOutEmailGenerate(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutCaptchaParse() { + handler.fire(.optOutCaptchaParse(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutCaptchaSend() { + handler.fire(.optOutCaptchaSend(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutCaptchaSolve() { + handler.fire(.optOutCaptchaSolve(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutSubmit() { + setStage(.submit) + handler.fire(.optOutSubmit(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutEmailReceive() { + handler.fire(.optOutEmailReceive(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutEmailConfirm() { + handler.fire(.optOutEmailConfirm(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutValidate() { + setStage(.validate) + handler.fire(.optOutValidate(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutSubmitSuccess() { + handler.fire(.optOutSubmitSuccess(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutFailure() { + handler.fire(.optOutFailure(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceStartTime(), stage: stage.rawValue)) + } + + func fireScanSuccess(matchesFound: Int) { + handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1)) + } + + func fireScanFailed() { + handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1)) + } + + func fireScanError(error: Error) { + var errorCategory: ErrorCategory = .unclassified + + if let dataBrokerProtectionError = error as? DataBrokerProtectionError { + switch dataBrokerProtectionError { + case .httpError(let httpCode): + if httpCode < 500 { + errorCategory = .clientError(httpCode: httpCode) + } else { + errorCategory = .serverError(httpCode: httpCode) + } + default: + errorCategory = .validationError + } + } else { + if let nsError = error as NSError? { + if nsError.domain == NSURLErrorDomain { + errorCategory = .networkError + } + } + } + + handler.fire( + .scanError( + dataBroker: dataBroker, + duration: durationSinceStartTime(), + category: errorCategory.toString, + details: error.localizedDescription + ) + ) + } + + // Helper methods to set the stage that is about to run. This help us + // identifying the stage so we can know which one was the one that failed. + + func setStage(_ stage: Stage) { + self.stage = stage + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index c61178c0f9..0eddaad17e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -32,6 +32,7 @@ final class DataBrokerProtectionProcessor { private let operationQueue: OperationQueue private var pixelHandler: EventMapping private let userNotificationService: DataBrokerProtectionUserNotificationService + private let engagementPixels: DataBrokerProtectionEngagementPixels init(database: DataBrokerProtectionRepository, config: SchedulerConfig, @@ -48,6 +49,7 @@ final class DataBrokerProtectionProcessor { self.pixelHandler = pixelHandler self.operationQueue.maxConcurrentOperationCount = config.concurrentOperationsDifferentBrokers self.userNotificationService = userNotificationService + self.engagementPixels = DataBrokerProtectionEngagementPixels(database: database, handler: pixelHandler) } // MARK: - Public functions @@ -111,6 +113,9 @@ final class DataBrokerProtectionProcessor { brokerUpdater.checkForUpdatesInBrokerJSONFiles() } + // This will fire the DAU/WAU/MAU pixels, + engagementPixels.fireEngagementPixel() + let brokersProfileData = database.fetchAllBrokerProfileQueryData() let dataBrokerOperationCollections = createDataBrokerOperationCollections(from: brokersProfileData, operationType: operationType, diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEngagementPixelsTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEngagementPixelsTests.swift new file mode 100644 index 0000000000..7779cc0d00 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEngagementPixelsTests.swift @@ -0,0 +1,220 @@ +// +// DataBrokerProtectionEngagementPixelsTests.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 XCTest +import Foundation +@testable import DataBrokerProtection + +final class DataBrokerProtectionEngagementPixelsTests: XCTestCase { + + private let database = MockDatabase() + private let repository = MockDataBrokerProtectionEngagementPixelsRepository() + private let handler = MockDataBrokerProtectionPixelsHandler() + + private var fakeProfile: DataBrokerProtectionProfile { + let name = DataBrokerProtectionProfile.Name(firstName: "John", lastName: "Doe") + let address = DataBrokerProtectionProfile.Address(city: "City", state: "State") + + return DataBrokerProtectionProfile(names: [name], addresses: [address], phones: [String](), birthYear: 1900) + } + + override func tearDown() { + database.clear() + repository.clear() + handler.clear() + } + + func testWhenThereIsNoProfile_thenNoEngagementPixelIsFired() { + database.setFetchedProfile(nil) + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: Date()) + + // We test we have no interactions with the repository + XCTAssertFalse(repository.wasDailyPixelSent) + XCTAssertFalse(repository.wasWeeklyPixelSent) + XCTAssertFalse(repository.wasMonthlyPixelSent) + XCTAssertFalse(repository.wasGetLatestDailyPixelCalled) + XCTAssertFalse(repository.wasWeeklyPixelSent) + XCTAssertFalse(repository.wasMonthlyPixelSent) + + // The pixel should not be fired + XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.isEmpty) + } + + func testWhenLatestDailyPixelIsNil_thenWeFireDailyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestDailyPixel = nil + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: Date()) + + XCTAssertTrue(wasPixelFired(.dailyActiveUser)) + XCTAssertTrue(repository.wasDailyPixelSent) + } + + func testWhenCurrentDayIsDifferentToLatestDailyPixel_thenWeFireDailyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestWeeklyPixel = dateFromString("2024-02-20") + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: dateFromString("2024-02-21")) + + XCTAssertTrue(wasPixelFired(.dailyActiveUser)) + XCTAssertTrue(repository.wasDailyPixelSent) + } + + func testWhenCurrentDayIsEqualToLatestDailyPixel_thenWeDoNotFireDailyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestDailyPixel = Date() + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: Date()) + + XCTAssertFalse(wasPixelFired(.dailyActiveUser)) + XCTAssertFalse(repository.wasDailyPixelSent) + } + + func testWhenLatestWeeklyPixelIsNil_thenWeFireWeeklyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestWeeklyPixel = nil + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: Date()) + + XCTAssertTrue(wasPixelFired(.weeklyActiveUser)) + XCTAssertTrue(repository.wasWeeklyPixelSent) + } + + func testWhenCurrentDayIsSevenDatesEqualOrGreaterThanLatestWeekly_thenWeFireWeeklyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestWeeklyPixel = dateFromString("2024-02-20") + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: dateFromString("2024-02-27")) + + XCTAssertTrue(wasPixelFired(.weeklyActiveUser)) + XCTAssertTrue(repository.wasWeeklyPixelSent) + } + + func testWhenCurrentDayIsSevenDatesLessThanLatestWeekly_thenWeDoNotFireWeeklyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestWeeklyPixel = dateFromString("2024-02-20") + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: dateFromString("2024-02-26")) + + XCTAssertFalse(wasPixelFired(.weeklyActiveUser)) + XCTAssertFalse(repository.wasWeeklyPixelSent) + } + + func testWhenLatestMonthlyPixelIsNil_thenWeFireMonthlyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestMonthlyPixel = nil + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: Date()) + + XCTAssertTrue(wasPixelFired(.monthlyActiveUser)) + XCTAssertTrue(repository.wasMonthlyPixelSent) + } + + func testWhenCurrentMonthIs28DatesGreaterOrEqualThanLatestMonthlyPixel_thenWeFireMonthlyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestMonthlyPixel = dateFromString("2024-02-20") + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: dateFromString("2024-03-19")) + + XCTAssertTrue(wasPixelFired(.monthlyActiveUser)) + XCTAssertTrue(repository.wasMonthlyPixelSent) + } + + func testWhenCurrentIsNot28DatesGreaterOrEqualToLatestMonthlyPixel_thenWeDoNotFireMonthlyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestMonthlyPixel = dateFromString("2024-02-20") + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: dateFromString("2024-03-18")) + + XCTAssertFalse(wasPixelFired(.monthlyActiveUser)) + XCTAssertFalse(repository.wasMonthlyPixelSent) + } + + private func wasPixelFired(_ pixel: DataBrokerProtectionPixels) -> Bool { + MockDataBrokerProtectionPixelsHandler.lastPixelsFired.contains(where: { $0.name == pixel.name }) + } + + private func dateFromString(_ string: String) -> Date { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + + return dateFormatter.date(from: string)! + } +} + +final class MockDataBrokerProtectionEngagementPixelsRepository: DataBrokerProtectionEngagementPixelsRepository { + var wasDailyPixelSent = false + var wasWeeklyPixelSent = false + var wasMonthlyPixelSent = false + var wasGetLatestDailyPixelCalled = false + var wasGetLatestWeeklyPixelCalled = false + var wasGetLatestMonthlyPixelCalled = false + var setLatestDailyPixel: Date? + var setLatestWeeklyPixel: Date? + var setLatestMonthlyPixel: Date? + + func markDailyPixelSent() { + wasDailyPixelSent = true + } + + func markWeeklyPixelSent() { + wasWeeklyPixelSent = true + } + + func markMonthlyPixelSent() { + wasMonthlyPixelSent = true + } + + func getLatestDailyPixel() -> Date? { + wasGetLatestDailyPixelCalled = true + return setLatestDailyPixel + } + + func getLatestWeeklyPixel() -> Date? { + wasGetLatestWeeklyPixelCalled = true + return setLatestWeeklyPixel + } + + func getLatestMonthlyPixel() -> Date? { + wasGetLatestMonthlyPixelCalled = true + return setLatestMonthlyPixel + } + + func clear() { + wasDailyPixelSent = false + wasWeeklyPixelSent = false + wasMonthlyPixelSent = false + wasGetLatestDailyPixelCalled = false + wasGetLatestWeeklyPixelCalled = false + wasGetLatestMonthlyPixelCalled = false + setLatestDailyPixel = nil + setLatestWeeklyPixel = nil + setLatestMonthlyPixel = nil + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index 12b7120077..db0607d891 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -47,7 +47,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { sut.calculateMismatches() - let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelFired! + let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! let pixelName = DataBrokerProtectionPixels.parentChildMatches(parent: "", child: "", value: 0).name XCTAssertEqual(lastPixel.name, pixelName) XCTAssertEqual(Int((lastPixel.params?["value"])!), @@ -72,7 +72,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { sut.calculateMismatches() - let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelFired! + let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! let pixelName = DataBrokerProtectionPixels.parentChildMatches(parent: "", child: "", value: 0).name XCTAssertEqual(lastPixel.name, pixelName) XCTAssertEqual(Int((lastPixel.params?["value"])!), @@ -97,7 +97,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { sut.calculateMismatches() - let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelFired! + let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! let pixelName = DataBrokerProtectionPixels.parentChildMatches(parent: "", child: "", value: 0).name XCTAssertEqual(lastPixel.name, pixelName) XCTAssertEqual(Int((lastPixel.params?["value"])!), @@ -122,7 +122,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { sut.calculateMismatches() - let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelFired! + let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! let pixelName = DataBrokerProtectionPixels.parentChildMatches(parent: "", child: "", value: 0).name XCTAssertEqual(lastPixel.name, pixelName) XCTAssertEqual(Int((lastPixel.params?["value"])!), @@ -143,7 +143,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { sut.calculateMismatches() - XCTAssertNil(MockDataBrokerProtectionPixelsHandler.lastPixelFired) + XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.isEmpty) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 95ea0d1493..575c0cdd17 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -620,11 +620,11 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault public class MockDataBrokerProtectionPixelsHandler: EventMapping { - static var lastPixelFired: DataBrokerProtectionPixels? + static var lastPixelsFired = [DataBrokerProtectionPixels]() public init() { super.init { event, _, _, _ in - MockDataBrokerProtectionPixelsHandler.lastPixelFired = event + MockDataBrokerProtectionPixelsHandler.lastPixelsFired.append(event) } } @@ -633,7 +633,7 @@ public class MockDataBrokerProtectionPixelsHandler: EventMapping DataBrokerProtectionProfile? { wasFetchProfileCalled = true - return nil + return profile + } + + func setFetchedProfile(_ profile: DataBrokerProtectionProfile?) { + self.profile = profile } func deleteProfileData() { @@ -802,6 +807,7 @@ final class MockDatabase: DataBrokerProtectionRepository { lastParentBrokerWhereChildSitesWhereFetched = nil lastProfileQueryIdOnScanUpdatePreferredRunDate = nil brokerProfileQueryDataToReturn.removeAll() + profile = nil } }