From e9bd56dca465c05a6cec74c233c9cdc1032771da Mon Sep 17 00:00:00 2001
From: Dominik Kapusta <dkapusta@duckduckgo.com>
Date: Fri, 15 Mar 2024 06:54:37 +0100
Subject: [PATCH 01/16] Fix determining if internal release should be
 automatically bumped

---
 .github/workflows/bump_internal_release.yml | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/bump_internal_release.yml b/.github/workflows/bump_internal_release.yml
index ba06acdcc6..1b9c4b3c4e 100644
--- a/.github/workflows/bump_internal_release.yml
+++ b/.github/workflows/bump_internal_release.yml
@@ -63,13 +63,21 @@ jobs:
             echo "skip-release=false" >> $GITHUB_OUTPUT
           else
             latest_tag="$(git describe --tags --abbrev=0)"
-            changed_files="$(git diff --name-only "$latest_tag".."origin/${release_branch}")"
+            latest_tag_sha="$(git rev-parse "$latest_tag")"
+            release_branch_sha="$(git rev-parse "origin/${release_branch}")"
 
-            if grep -q -v -e '.github' -e 'scripts' <<< "$changed_files"; then
-              echo "skip-release=false" >> $GITHUB_OUTPUT
-            else
-              echo "::warning::No changes to the release branch (or only changes to scripts and workflows). Skipping automatic release."
+            if [[ "${latest_tag_sha}" == "${release_branch_sha}" ]]; then
+              echo "::warning::Release branch's HEAD is already tagged. Skipping automatic release."
               echo "skip-release=true" >> $GITHUB_OUTPUT
+            else
+              changed_files="$(git diff --name-only "$latest_tag".."origin/${release_branch}")"
+              if grep -q -v -e '.github' -e 'scripts' <<< "$changed_files"; then
+                echo "::warning::New code changes found in the release branch since the last release. Will bump internal release now."
+                echo "skip-release=false" >> $GITHUB_OUTPUT
+              else
+                echo "::warning::No changes to the release branch (or only changes to scripts and workflows). Skipping automatic release."
+                echo "skip-release=true" >> $GITHUB_OUTPUT
+              fi
             fi
           fi
 

From a40035b73906caff097f53471521d274de2696fe Mon Sep 17 00:00:00 2001
From: Juan Manuel Pereira <juanmanuel.pereira1@gmail.com>
Date: Fri, 15 Mar 2024 08:38:32 -0300
Subject: [PATCH 02/16] DBP: Use url instead of name to identify brokers on
 pixels (#2423)

---
 .../Operations/DataBrokerProfileQueryOperationManager.swift   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift
index ff931a7687..58352c3fc3 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift
@@ -117,7 +117,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager {
             notificationCenter.post(name: DataBrokerProtectionNotifications.didFinishScan, object: brokerProfileQueryData.dataBroker.name)
         }
 
-        let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, handler: pixelHandler)
+        let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.url, handler: pixelHandler)
 
         do {
             let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .scanStarted)
@@ -280,7 +280,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager {
         }
 
         let retriesCalculatorUseCase = OperationRetriesCalculatorUseCase()
-        let stageDurationCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, handler: pixelHandler)
+        let stageDurationCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.url, handler: pixelHandler)
         stageDurationCalculator.fireOptOutStart()
         os_log("Running opt-out operation: %{public}@", log: .dataBrokerProtection, String(describing: brokerProfileQueryData.dataBroker.name))
 

From 919a93baf974e56a9b69b440b2b3918757bc7644 Mon Sep 17 00:00:00 2001
From: Juan Manuel Pereira <juanmanuel.pereira1@gmail.com>
Date: Fri, 15 Mar 2024 08:39:00 -0300
Subject: [PATCH 03/16] DBP: Treat 404 as failure instead of error (#2426)

---
 ...kerProtectionStageDurationCalculator.swift |   7 +-
 ...otectionStageDurationCalculatorTests.swift | 141 ++++++++++++++++++
 2 files changed, 147 insertions(+), 1 deletion(-)
 create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift

diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStageDurationCalculator.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStageDurationCalculator.swift
index 7de439e30b..36723e14bf 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStageDurationCalculator.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionStageDurationCalculator.swift
@@ -170,7 +170,12 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator
             switch dataBrokerProtectionError {
             case .httpError(let httpCode):
                 if httpCode < 500 {
-                    errorCategory = .clientError(httpCode: httpCode)
+                    if httpCode == 404 {
+                        fireScanFailed()
+                        return
+                    } else {
+                        errorCategory = .clientError(httpCode: httpCode)
+                    }
                 } else {
                     errorCategory = .serverError(httpCode: httpCode)
                 }
diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift
new file mode 100644
index 0000000000..8b899e12a1
--- /dev/null
+++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift
@@ -0,0 +1,141 @@
+//
+//  DataBrokerProtectionStageDurationCalculatorTests.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 BrowserServicesKit
+import Foundation
+import XCTest
+
+@testable import DataBrokerProtection
+
+final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase {
+    let handler = MockDataBrokerProtectionPixelsHandler()
+
+    override func tearDown() {
+        handler.clear()
+    }
+
+    func testWhenErrorIs404_thenWeFireScanFailedPixel() {
+        let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler)
+
+        sut.fireScanError(error: DataBrokerProtectionError.httpError(code: 404))
+
+        XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1)
+
+        if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{
+            switch failurePixel {
+            case .scanFailed(let broker, _, _):
+                XCTAssertEqual(broker, "broker")
+            default: XCTFail("The scan failed pixel should be fired")
+            }
+        } else {
+            XCTFail("A pixel should be fired")
+        }
+    }
+
+    func testWhenErrorIs403_thenWeFireScanErrorPixelWithClientErrorCategory() {
+        let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler)
+
+        sut.fireScanError(error: DataBrokerProtectionError.httpError(code: 403))
+
+        XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1)
+
+        if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{
+            switch failurePixel {
+            case .scanError(_, _, let category, _):
+                XCTAssertEqual(category, ErrorCategory.clientError(httpCode: 403).toString)
+            default: XCTFail("The scan error pixel should be fired")
+            }
+        } else {
+            XCTFail("A pixel should be fired")
+        }
+    }
+
+    func testWhenErrorIs500_thenWeFireScanErrorPixelWithServerErrorCategory() {
+        let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler)
+
+        sut.fireScanError(error: DataBrokerProtectionError.httpError(code: 500))
+
+        XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1)
+
+        if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{
+            switch failurePixel {
+            case .scanError(_, _, let category, _):
+                XCTAssertEqual(category, ErrorCategory.serverError(httpCode: 500).toString)
+            default: XCTFail("The scan error pixel should be fired")
+            }
+        } else {
+            XCTFail("A pixel should be fired")
+        }
+    }
+
+    func testWhenErrorIsNotHttp_thenWeFireScanErrorPixelWithValidationErrorCategory() {
+        let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler)
+
+        sut.fireScanError(error: DataBrokerProtectionError.actionFailed(actionID: "Action-ID", message: "Some message"))
+
+        XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1)
+
+        if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{
+            switch failurePixel {
+            case .scanError(_, _, let category, _):
+                XCTAssertEqual(category, ErrorCategory.validationError.toString)
+            default: XCTFail("The scan error pixel should be fired")
+            }
+        } else {
+            XCTFail("A pixel should be fired")
+        }
+    }
+
+    func testWhenErrorIsNotDBPErrorButItIsNSURL_thenWeFireScanErrorPixelWithNetworkErrorErrorCategory() {
+        let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler)
+        let nsURLError = NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut)
+
+        sut.fireScanError(error: nsURLError)
+
+        XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1)
+
+        if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{
+            switch failurePixel {
+            case .scanError(_, _, let category, _):
+                XCTAssertEqual(category, ErrorCategory.networkError.toString)
+            default: XCTFail("The scan error pixel should be fired")
+            }
+        } else {
+            XCTFail("A pixel should be fired")
+        }
+    }
+
+    func testWhenErrorIsNotDBPErrorAndNotURL_thenWeFireScanErrorPixelWithUnclassifiedErrorCategory() {
+        let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler)
+        let error = NSError(domain: NSCocoaErrorDomain, code: -1)
+
+        sut.fireScanError(error: error)
+
+        XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1)
+
+        if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{
+            switch failurePixel {
+            case .scanError(_, _, let category, _):
+                XCTAssertEqual(category, ErrorCategory.unclassified.toString)
+            default: XCTFail("The scan error pixel should be fired")
+            }
+        } else {
+            XCTFail("A pixel should be fired")
+        }
+    }
+}

From aaacc14afca2f61dd36314bb09f6837b344713bb Mon Sep 17 00:00:00 2001
From: Michal Smaga <miasma13@gmail.com>
Date: Fri, 15 Mar 2024 12:49:25 +0100
Subject: [PATCH 04/16] Properly handle edge cases (#2417)

Task/Issue URL: https://app.asana.com/0/0/1206841322374471/f

**Description**:
- Handle edge cases via "Something Went Wrong" alert
- Handle scenario when App Store purchase went through but the BE failed
- Fix description in the settings detailing subscription's billing
period and expiry or renewal date
- Handle expired subscription state

**Steps to test this PR**:
Test triggering "Something Went Wrong" alert, e.g.:
1. Opening purchase page
2. Disable internet connection
3. Try purchasing

Handle scenario when App Store purchase went through but the BE failed
1. Modify `SubscriptionPagesUserScript.swift` commenting call to
`AppStorePurchaseFlow.completeSubscriptionPurchase` and the whole
`switch` - but you need to leave `await
pushPurchaseUpdate(originalMessage: message, purchaseUpdate:
PurchaseUpdate(type: "completed"))`
2. Switch purchase platform to App Store
3. Try purchasing subscription
5. Due to code modification the confirmation endpoint will not be used
leaving you with an account lacking entitlements
6. Go to settings

Fix description in the settings detailing subscription's billing period
and expiry or renewal date
1. Purchase or activate subscription
2. Open settings, the description should properly reflect if
subscription will renew or expire at given date

Handle expired subscription state
1. Purchase subscription via App Store
3. Wait until it expires
4. Open settings

<!--
Tagging instructions
If this PR isn't ready to be merged for whatever reason it should be
marked with the `DO NOT MERGE` label (particularly if it's a draft)
If it's pending Product Review/PFR, please add the `Pending Product
Review` label.

If at any point it isn't actively being worked on/ready for
review/otherwise moving forward (besides the above PR/PFR exception)
strongly consider closing it (or not opening it in the first place). If
you decide not to close it, make sure it's labelled to make it clear the
PRs state and comment with more information.
-->

---
###### 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)
---
 .../Images/ITR-Icon.imageset/ITR-Icon.pdf     | Bin 4129 -> 3919 bytes
 .../SubscriptionIcon.pdf                      | Bin 2646 -> 4808 bytes
 DuckDuckGo/Common/Localizables/UserText.swift |   1 -
 .../NavigationBar/View/MoreOptionsMenu.swift  |   9 +-
 .../SubscriptionPagesUserScript.swift         |  12 +-
 .../SubscriptionUI/NSAlert+Subscription.swift |   8 -
 .../PreferencesSubscriptionModel.swift        | 115 ++++--
 .../PreferencesSubscriptionView.swift         | 352 ++++++++++++------
 .../Contents.json                             |  12 +
 .../subscription-expired-icon.pdf             | Bin 0 -> 2374 bytes
 .../Contents.json                             |  12 +
 .../Info-Color-16.pdf                         | Bin 0 -> 1745 bytes
 .../ActivateSubscriptionAccessModel.swift     |   5 +-
 .../Model/ShareSubscriptionAccessModel.swift  |   2 +-
 .../SubscriptionAccessView.swift              |   1 +
 .../Sources/SubscriptionUI/UserText.swift     |  55 ++-
 16 files changed, 407 insertions(+), 177 deletions(-)
 create mode 100644 LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/Contents.json
 create mode 100644 LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/subscription-expired-icon.pdf
 create mode 100644 LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Contents.json
 create mode 100644 LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Info-Color-16.pdf

diff --git a/DuckDuckGo/Assets.xcassets/Images/ITR-Icon.imageset/ITR-Icon.pdf b/DuckDuckGo/Assets.xcassets/Images/ITR-Icon.imageset/ITR-Icon.pdf
index 1ff6a4f6db7f5eb49a1d732bf7d6ebef1fbc4c10..71e908133d48e1c57639464825a9722d736aefbe 100644
GIT binary patch
delta 143
zcmZ3ea9(bL_{6h*o0S;NStlzo>Tm96>tX~l^fvo(D6xP=mhr?hG8>s&Z06uAWb`vI
tHc>DD0fjs+Fl}ILY-WKWWNC;lWNL<?(ad=AasEI~GfQ(WRaIAiHvlNmAr=4t

delta 353
zcmX>vw@_h%c)h-BMTvWGNn%N=f{l%WzN-nCN={TT(6cZx00IR&J1zxGpzmr#v`S;#
zD$P(#)^|0;trVfs&TgWZKO0DKdhz7htOgsWaIgZoMnLZ7*{r)5fox-ll>X*sb|n@d
z-vp|3^A+xRMrLCJv(09_g^YeihGq&NPZ}uXae-+AV^ebrbRjbXLv$f?a}#tS3zNxT
Q`2smD4a~VzRbBnv02LET*Z=?k

diff --git a/DuckDuckGo/Assets.xcassets/Images/SubscriptionIcon.imageset/SubscriptionIcon.pdf b/DuckDuckGo/Assets.xcassets/Images/SubscriptionIcon.imageset/SubscriptionIcon.pdf
index 934a29304855a8da122da590fa025cc53f443972..d9be8eca8e50aae5d9b27cf4e2e48715bd2c614e 100644
GIT binary patch
literal 4808
zcmd^@OOG2z5`^#lEBYe9UO>%yKLA63^@QOytex3|0b>soMN4DPkXn*j$ouQ}i|S^R
zoYBJa*}SMGx-u&(GBT@5k6u52`PdG*>uhiO?VDe_zW?T%e(}%WKdk%3Pd~o-b-g-(
z@mu_Ty?J+d-#aE=P0*{|_Tz^$gZB6Mt=Wu**ZA8lH~SCE-Fmb7{o?zRCzGq|k8cib
z(U;rZ-Gp7O_uG%V)q1Zb=9y2QZa?)u_hvBdcN*AqgifP>;R?(BVYgo1b&Kcg|J<zB
zSFfJ-&#uq^TJ5`kclKyLyM8>6=5K<m$-(ty@Fiti`_)}H&wDu7uQpw2HpTUR@F}=h
z`a3WmV@>9Sl_B|<+bmN;@QEcR#0}Y3*{!gfFf-VkGT3cr?O<zhm|lW0_`nJ?$h_jb
z=CKWHyh)+Iz(OdFt*zwoG&5HpoYBn5A(AoKYbMyV@(DYO@dVzU0)On=7O=hqYcbVX
z@j4RoI1DaLREu+kJwvN54=KfD!Wo!JNn?;DCU57Eh-<q$f%8UgyD;zg0t~rWEi!{)
z-mw`bX3n9R!gFxqIt-JyIV1C;Gop=6yO^=t6E-X14D-XJmzk1TXAhOP>fU$1berxU
ze_rttjrA_tFOYm1V$8{-kcetbFb>V9q552M1FQ2PraliL)l^bX&03dSsR+QBg4{<a
zR75IC#gI*{Bj^*=Q4wLT#%qaci_HNjpq-lAs@_#6Q%l&<h7@}%x`V5%M($ElV!=X)
z#(>$uqjV}7%taH31#34LXANZpYGQ2RlZC{R&7-m2R7M^(<Qxf`%m-iDCe~q47^)t8
zS{oG`QRGsCZ60&>krm_MDJjDUKQTt1WgV&eQZupnA$aRR)!S^07R*6sg5`!e<3xp1
zSE@msLNlYQHo#~MH7dwBgz6L|-%>6C3*ur0YkCI7kO7$@DG~)y4n~W?hbrBaL>?`n
zlvpfmI~#*7G#DmT{I9x7SAyUm%x8l)5N}vth>fP%nNn=)RuE2Rnw^1JBVVLG#Xxf)
z!F1RxOR(VmghhI2!^nWzUs2Y_I7~)64pn6`mgUWmAL!5Q*Ok=#y9^tw2?o0^%a^ao
zTYN<}PS;_-V2RH#P)k$>p|%Eu#HZ{S`XghKi5a3R#hZy(XwpgHt@jiCp{Nln8tP|E
zQ(|kfM0_Hu<ujJ#nMaSd#M9C)`2l5>9_&jWA$o0qlmr@M6Re@yiegI@gQhL)Ds;QS
z;cQH`4OF7|_9#4M*VL+*Ok_94<fEulz%x+tq582RQZ%ieqS`VFm2^Ua%FxFF(RtHW
zEfh4G{7eH->rrt>#~xJ$k!p+d1=5uZ-Y2kTPrgu?T0ZkQ=ET2R1yr#|!U~M(L$s8X
za)MiUVP!uCQ))?#D(|gLmiP=F#_BTBBR)(iQ*{8Uf^yC%EpZmZS(d5#EPey*A)Hp2
zZYXpQ8i9x}0Zq4htN2hg4#}mYqGle=Ls_BS(`8y_spG)<G9{*3C}f8Pb-?<A)8__J
zo{roKa)tiLEYBfN`N-6f6k^vMfTGm2TuDHL?T{>;f^aj(DBUq2+Z<>u$~B%0<-@T+
zj{=Jj$mS*Pkb`Bwq2st<DCpFx8ao|Ccx6i@qY|r9KtDzWATe7MLY}N^J&^(k%3M2{
zSw|gHQBkebRi;-3L;^-ixVf<jt|A-^LfAYaqfK#8iW+>e=c(_bVx1#k$#OqXy+`!S
zQo^!mov@-FFN}#J7E5V+ie$uT9*sX%+KM_Gjg9UjifyL96!-~-N+e+jA(&+Ik;c{(
zX^^DY`H^ipR}*J35y7;SptS~Rqf_co*kqd#gZc`TbCny5HgOE-IFuABty<Q|ifDzY
z_cx`2s^8G5Kno-}_KAGenA=P=h%^;;jm1#|n#TZ-$c8cfsx-CYvsE7oM)RsK)*%e7
z%@xO{0!{caoJy!4YfevYf65&4p*gme?jy|M5NV-Tj&B_?B`9|)PI%J!MPqg>LO3eo
zXz3Ya*2lg>Tjjb2^q?iQOUmEcEu~v;-cHwdMtY)jdV-8^6<2H0ga*?$zw8Oa;q+I2
z!sx)!uN_Bi@W(Hh+2-`Lx@#AV(bEZ~!sch#Y;^mX2htgKvbdcur=Ll?(l({tyE*G|
zM)h!=o~-Rn`V*2?rcY1D7oQHVt`Ez@x_y6P+dtPoEcd^)(fI1spBHb`(^K|xw0PA^
za?@k@^4o*I7QBMLz5Vch`Nyu0FWm<|1WZr!c_;tR-IFKdwsCf>k95J*^qlANGcxo=
zPvixhz1w#SsC~ZW3GtaM$>Er#a4Jo%U1z2F?Pjw*?E9bfI6rOa_43_%0)AM(y;=U*
zXTCh0{>MPFIQ}=qv+d@v-msz=ID)VBzY&L?%VmSkcoH=3K#KbQ<}IQ2+QC(SjQH91
zBewQI{CkiyPrA3AxzY~z^NOdPKU*G_x7&A@wx4$EUpkt*?OKVAb(Xkx#U6b6X#xHE
z%>;2pq)hHXnVeignYMc$;zYWH0?(`a5C`Rbh<nw2$a6N|^A)(>9HGN*d2_qob)xsX
z=aBo=etWn6y8r6t?bjp4i>vJxennr)*W&u-ziV*;x%<O%cWCVJY|GuFM=ySS`9G$N
B-@X6<

literal 2646
zcmZveTW=gi5QX38SM*Dy1S##RzPA)bBF;q!5XRvx;$c}&93y)dyc-nw^?cQ{UdKsB
zmfb$nm#TA4RnMcdm#?3yuhXPX-1d(@C+D6$b0@Dp?ccoJulDoupN>E4l@s5R@VMvC
zm!E0gT-dTHJXrtJyt&+ea1rpFiPLU>JFjl0lb7>9SL^xw%?o$>_VfSNyXo)g<dwS1
z-K5@5;0(DNJUx!S{jT=VvRmIw$9Z25cI(Yld}^WA=rsFOYH7{g0Jf&R1p`~@Evu_O
z#acshTD&wVxd8QEd&vXhyjtCQYCh){q%L6X#N54V=t-8{QcM9TOYgCdQ!^)Zma2Gd
zp|#>P`C5}^mWZ!~V~8*Lq^Tq#P#;T(Q4I@GdTF*=ea|HlKd4wL%nZKN5G#p*Nz8$`
zB3M-AZSoqWF^|>k6pC2fj6{PjQCN^;Bt?5<P4*$xo=7@+=;%4v>g+4qLpUgZ#NKD<
ziNF>`D>2uRZSSoVM!^SIY~ySgB?}eTeB!l?V>qQmalC^PsI|CSQrKaTG{~SV@;b*L
zF8G*R6?jswS&PDhPtpVAbXe*Mj>V@WF<QtbN|D~c(6z<rt_K$3cwoimU884j7z8W(
z2wM<nSgf&#trgKoI@Om@LaPgs)DYo-kPqufem!heGcnqk87A1vtd=_YjW`178CHwr
zYH?V+1b#gws;mu$m#hjI_nHbOJ*1AvzG`kLw=e{uYwVwcP@e}PV{ukF$j-t+cWTCq
z<pBqaEU-LQij>d?*hmiBiVwYO0y3FnZ&r{stLX3*R_l!LgV&Dp55|!Wlf+iFqWAEZ
z<US+|DTZNKsO`kIp4d35q~UELa9*38Idy?D=1M>%5ij1dR8yxG))Ur@DKGo+7SB1#
zR}pf!$NYpa-zfO?^Z`v#h68O!m8!kc2kt@Ns7LDc+N!h!ZC&BLEuzC3<)QFRZD|fT
z2!e#V>wyr2otFZSn5meD2!h%uBd!dmG?C^DvM`STsDjaID3VeeJ{biH$|0svgnC5<
zX3_|(r~~b(i<v(8n2NOoupDx+F1VC80uUH2fe<SN3a?p)nqZ|=Yb}vd4UjnICG2R#
zvZhkW${NnGXxSY>;RMT%b6ROcCcbNvHY`)=YAC>J+FELAZWyEpQFM%5b3wmI3bfit
z{$B}Tx>O$yx-1eTWVy%CDkRhxMZ<nkh=MJV2PUm~mntN~!3dPIsnI*Vz@o+)I-HJ&
zq7>^{P=>GRjnN100?Kz}+;BZ|Q|dJg0ZIy$StRE>-V6(`rO=lbr#I_FMs=zbiTVQN
zM@^7yEX`QJD#ygb7aYd5fVD=;MB{8~8)R1tvoHg;A4?OB6?V-ul&A$q0b55|oJFUm
zJrsh}g8``(E3VE3(Jj5<Q{y96TxHXTnInzsOTtZt_uoyM>DS4w_QSn>cgfp*zI-Go
z#yH;n=?L4m!}pubcE5A)e&<_)=j6A4|C-&&+3IpW06)(cSF0D>Pww4lMi1|LyK=`f
zF4bjpx6}E&+kU)V&pT&dE9ZypPPd!=yx~Oq1i1sxR<~e|mOLOnLGE%e=>9|J$EyoM
zb(X=oyF2G}`;oQ!Rs1`U&vv3`f3}k)xH}$rpZMu&zq;ODK3w|gc78vJtJ9LJ{dOhi
zm>uxpp~Z}m+Z>@voc0_cZbtHX2$|2nfC85O0*bVuJ7|Bqy1JfkCsXf-m(cNex4oI4
rxW`u)PZm;6&bQmWvxEo1CvUI*n~8pW*{yE(BOQt}O^+VE`sMZi<vbvv

diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift
index a5149fcee7..555dece7ae 100644
--- a/DuckDuckGo/Common/Localizables/UserText.swift
+++ b/DuckDuckGo/Common/Localizables/UserText.swift
@@ -1008,7 +1008,6 @@ struct UserText {
 
     // "data-broker-protection.optionsMenu" - Menu item data broker protection feature
     static let dataBrokerProtectionOptionsMenuItem = "Personal Information Removal"
-    static let dataBrokerProtectionScanOptionsMenuItem = "Personal Information Removal Scan"
     // "tab.dbp.title" - Tab data broker protection title
     static let tabDataBrokerProtectionTitle = "Personal Information Removal"
 
diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift
index 6ff8bfea32..b2dd2d0a7f 100644
--- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift
+++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift
@@ -344,7 +344,6 @@ final class MoreOptionsMenu: NSMenu {
             networkProtectionItem = makeNetworkProtectionItem()
 
             items.append(networkProtectionItem)
-
 #if SUBSCRIPTION
             if DefaultSubscriptionFeatureAvailability().isFeatureAvailable() && AccountManager().isUserAuthenticated {
                 Task {
@@ -432,19 +431,13 @@ final class MoreOptionsMenu: NSMenu {
 
 #if SUBSCRIPTION
     private func makeInactiveSubscriptionItems() -> [NSMenuItem] {
-        let dataBrokerProtectionItem = NSMenuItem(title: UserText.dataBrokerProtectionScanOptionsMenuItem,
-                                                  action: #selector(openSubscriptionPurchasePage(_:)),
-                                                  keyEquivalent: "")
-            .targetting(self)
-            .withImage(.dbpIcon)
-
         let privacyProItem = NSMenuItem(title: UserText.subscriptionOptionsMenuItem,
                                         action: #selector(openSubscriptionPurchasePage(_:)),
                                         keyEquivalent: "")
             .targetting(self)
             .withImage(.subscriptionIcon)
 
-        return [dataBrokerProtectionItem, privacyProItem]
+        return [privacyProItem]
     }
 #endif
 
diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift
index b0c6391e44..a69c43fa24 100644
--- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift
+++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift
@@ -305,8 +305,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature {
                 case .accountCreationFailed:
                     report(subscriptionActivationError: .accountCreationFailed)
                 }
-
-                return nil
+                await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled"))
             }
         }
 
@@ -524,15 +523,10 @@ extension SubscriptionPagesUseSubscriptionFeature {
 extension MainWindowController {
 
     @MainActor
-    func showSomethingWentWrongAlert(environment: SubscriptionPurchaseEnvironment.Environment = SubscriptionPurchaseEnvironment.current) {
+    func showSomethingWentWrongAlert() {
         guard let window else { return }
 
-        switch environment {
-        case .appStore:
-            window.show(.somethingWentWrongAlert())
-        case .stripe:
-            window.show(.somethingWentWrongStripeAlert())
-        }
+        window.show(.somethingWentWrongAlert())
     }
 
     @MainActor
diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift
index d79042d247..7080cdd43f 100644
--- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift
+++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/NSAlert+Subscription.swift
@@ -29,14 +29,6 @@ public extension NSAlert {
         return alert
     }
 
-    static func somethingWentWrongStripeAlert() -> NSAlert {
-        let alert = NSAlert()
-        alert.messageText = UserText.somethingWentWrongAlertTitle
-        alert.informativeText = UserText.somethingWentWrongStripeAlertDescription
-        alert.addButton(withTitle: UserText.okButtonTitle)
-        return alert
-    }
-
     static func subscriptionNotFoundAlert() -> NSAlert {
         let alert = NSAlert()
         alert.messageText = UserText.subscriptionNotFoundAlertTitle
diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift
index 8a0392ec37..c2cd52eba6 100644
--- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift
+++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift
@@ -18,11 +18,14 @@
 
 import AppKit
 import Subscription
+import struct Combine.AnyPublisher
+import enum Combine.Publishers
 
 public final class PreferencesSubscriptionModel: ObservableObject {
 
     @Published var isUserAuthenticated: Bool = false
     @Published var subscriptionDetails: String?
+    @Published var subscriptionStatus: Subscription.Status?
 
     @Published var hasAccessToVPN: Bool = false
     @Published var hasAccessToDBP: Bool = false
@@ -58,6 +61,28 @@ public final class PreferencesSubscriptionModel: ObservableObject {
              removeSubscriptionClick
     }
 
+    lazy var statePublisher: AnyPublisher<PreferencesSubscriptionState, Never> = {
+        let isSubscriptionActivePublisher = $subscriptionStatus.map {
+            $0 != .expired && $0 != .inactive
+        }.eraseToAnyPublisher()
+
+        let hasAnyEntitlementPublisher = Publishers.CombineLatest3($hasAccessToVPN, $hasAccessToDBP, $hasAccessToITR).map {
+            return $0 || $1 || $2
+        }.eraseToAnyPublisher()
+
+        return Publishers.CombineLatest3($isUserAuthenticated, isSubscriptionActivePublisher, hasAnyEntitlementPublisher)
+            .map { isUserAuthenticated, isSubscriptionActive, hasAnyEntitlement in
+                switch (isUserAuthenticated, isSubscriptionActive, hasAnyEntitlement) {
+                case (false, _, _): return PreferencesSubscriptionState.noSubscription
+                case (true, false, _): return PreferencesSubscriptionState.subscriptionExpired
+                case (true, true, false): return PreferencesSubscriptionState.subscriptionPendingActivation
+                case (true, true, true): return PreferencesSubscriptionState.subscriptionActive
+                }
+            }
+            .removeDuplicates()
+            .eraseToAnyPublisher()
+    }()
+
     public init(openURLHandler: @escaping (URL) -> Void,
                 userEventHandler: @escaping (UserEvent) -> Void,
                 sheetActionHandler: SubscriptionAccessActionHandlers,
@@ -72,9 +97,32 @@ public final class PreferencesSubscriptionModel: ObservableObject {
 
         if let token = accountManager.accessToken {
             Task {
-                let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token)
+                let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token, cachePolicy: .returnCacheDataElseLoad)
                 if case .success(let subscription) = subscriptionResult {
-                    self.updateDescription(for: subscription.expiresOrRenewsAt)
+                    self.updateDescription(for: subscription.expiresOrRenewsAt, status: subscription.status, period: subscription.billingPeriod)
+                    self.subscriptionPlatform = subscription.platform
+                    self.subscriptionStatus = subscription.status
+                }
+
+                switch await self.accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .returnCacheDataElseLoad) {
+                case let .success(result):
+                    self.hasAccessToVPN = result
+                case .failure:
+                    self.hasAccessToVPN = false
+                }
+
+                switch await self.accountManager.hasEntitlement(for: .dataBrokerProtection, cachePolicy: .returnCacheDataElseLoad) {
+                case let .success(result):
+                    self.hasAccessToDBP = result
+                case .failure:
+                    self.hasAccessToDBP = false
+                }
+
+                switch await self.accountManager.hasEntitlement(for: .identityTheftRestoration, cachePolicy: .returnCacheDataElseLoad) {
+                case let .success(result):
+                    self.hasAccessToITR = result
+                case .failure:
+                    self.hasAccessToITR = false
                 }
             }
         }
@@ -123,6 +171,7 @@ public final class PreferencesSubscriptionModel: ObservableObject {
 
     @MainActor
     func changePlanOrBillingAction() async -> ChangePlanOrBillingAction {
+
         switch subscriptionPlatform {
         case .apple:
             if await confirmIfSignedInToSameAccount() {
@@ -199,7 +248,20 @@ public final class PreferencesSubscriptionModel: ObservableObject {
         openURLHandler(.subscriptionFAQ)
     }
 
-    // swiftlint:disable cyclomatic_complexity
+    @MainActor
+    func refreshSubscriptionPendingState() {
+        if SubscriptionPurchaseEnvironment.current == .appStore {
+            if #available(macOS 12.0, *) {
+                Task {
+                    _ = await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: subscriptionAppGroup)
+                    fetchAndUpdateSubscriptionDetails()
+                }
+            }
+        } else {
+            fetchAndUpdateSubscriptionDetails()
+        }
+    }
+
     @MainActor
     func fetchAndUpdateSubscriptionDetails() {
         guard fetchSubscriptionDetailsTask == nil else { return }
@@ -211,22 +273,12 @@ public final class PreferencesSubscriptionModel: ObservableObject {
 
             guard let token = self?.accountManager.accessToken else { return }
 
-            let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token)
+            let subscriptionResult = await SubscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData)
 
             if case .success(let subscription) = subscriptionResult {
-                self?.updateDescription(for: subscription.expiresOrRenewsAt)
+                self?.updateDescription(for: subscription.expiresOrRenewsAt, status: subscription.status, period: subscription.billingPeriod)
                 self?.subscriptionPlatform = subscription.platform
-
-                if subscription.expiresOrRenewsAt.timeIntervalSinceNow < 0 || !subscription.isActive {
-                    self?.hasAccessToVPN = false
-                    self?.hasAccessToDBP = false
-                    self?.hasAccessToITR = false
-
-                    if !subscription.isActive {
-                        self?.accountManager.signOut()
-                        return
-                    }
-                }
+                self?.subscriptionStatus = subscription.status
             } else {
                 self?.accountManager.signOut()
             }
@@ -255,16 +307,33 @@ public final class PreferencesSubscriptionModel: ObservableObject {
             }
         }
     }
-    // swiftlint:enable cyclomatic_complexity
 
-    private func updateDescription(for date: Date) {
-        self.subscriptionDetails = UserText.preferencesSubscriptionActiveCaption(formattedDate: dateFormatter.string(from: date))
+    func updateDescription(for date: Date, status: Subscription.Status, period: Subscription.BillingPeriod) {
+
+        let formattedDate = dateFormatter.string(from: date)
+
+        let billingPeriod: String
+
+        switch period {
+        case .monthly: billingPeriod = UserText.monthlySubscriptionBillingPeriod.lowercased()
+        case .yearly: billingPeriod = UserText.yearlySubscriptionBillingPeriod.lowercased()
+        case .unknown: billingPeriod = ""
+        }
+
+        switch status {
+        case .autoRenewable:
+            self.subscriptionDetails = UserText.preferencesSubscriptionActiveRenewCaption(period: billingPeriod, formattedDate: formattedDate)
+        case .expired, .inactive:
+            self.subscriptionDetails = UserText.preferencesSubscriptionExpiredCaption(formattedDate: formattedDate)
+        default:
+            self.subscriptionDetails = UserText.preferencesSubscriptionActiveExpireCaption(period: billingPeriod, formattedDate: formattedDate)
+        }
     }
 
     private var dateFormatter = {
         let dateFormatter = DateFormatter()
-        dateFormatter.dateStyle = .medium
-        dateFormatter.timeStyle = .short
+        dateFormatter.dateStyle = .long
+        dateFormatter.timeStyle = .none
 
         return dateFormatter
     }()
@@ -277,3 +346,7 @@ enum ManageSubscriptionSheet: Identifiable {
         return self
     }
 }
+
+enum PreferencesSubscriptionState: String {
+    case noSubscription, subscriptionPendingActivation, subscriptionActive, subscriptionExpired
+}
diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift
index f17fcb0ea8..5424f3920b 100644
--- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift
+++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift
@@ -21,6 +21,9 @@ import SwiftUI
 import SwiftUIExtensions
 
 public struct PreferencesSubscriptionView: View {
+
+    @State private var state: PreferencesSubscriptionState = .noSubscription
+
     @ObservedObject var model: PreferencesSubscriptionModel
     @State private var showingSheet = false
     @State private var showingRemoveConfirmationDialog = false
@@ -39,41 +42,14 @@ public struct PreferencesSubscriptionView: View {
                     SubscriptionAccessView(model: model.sheetModel)
                 }
                 .sheet(isPresented: $showingRemoveConfirmationDialog) {
-                    SubscriptionDialog(imageName: "Privacy-Pro-128",
-                                       title: UserText.removeSubscriptionDialogTitle,
-                                       description: UserText.removeSubscriptionDialogDescription,
-                                       buttons: {
-                        Button(UserText.removeSubscriptionDialogCancel) { showingRemoveConfirmationDialog = false }
-                        Button(action: {
-                            showingRemoveConfirmationDialog = false
-                            model.removeFromThisDeviceAction()
-                        }, label: {
-                            Text(UserText.removeSubscriptionDialogConfirm)
-                                .foregroundColor(.red)
-                        })
-                    })
-                    .frame(width: 320)
+                    removeConfirmationDialog
                 }
                 .sheet(item: $manageSubscriptionSheet) { sheet in
                     switch sheet {
                     case .apple:
-                        SubscriptionDialog(imageName: "app-store",
-                                           title: UserText.changeSubscriptionDialogTitle,
-                                           description: UserText.changeSubscriptionAppleDialogDescription,
-                                           buttons: {
-                            Button(UserText.changeSubscriptionDialogDone) { manageSubscriptionSheet = nil }
-                                .buttonStyle(DefaultActionButtonStyle(enabled: true))
-                        })
-                        .frame(width: 360)
+                        manageSubscriptionAppStoreDialog
                     case .google:
-                        SubscriptionDialog(imageName: "google-play",
-                                           title: UserText.changeSubscriptionDialogTitle,
-                                           description: UserText.changeSubscriptionGoogleDialogDescription,
-                                           buttons: {
-                            Button(UserText.changeSubscriptionDialogDone) { manageSubscriptionSheet = nil }
-                                .buttonStyle(DefaultActionButtonStyle(enabled: true))
-                        })
-                        .frame(width: 360)
+                        manageSubscriptionGooglePlayDialog
                     }
                 }
 
@@ -81,93 +57,26 @@ public struct PreferencesSubscriptionView: View {
                 .frame(height: 20)
 
             VStack {
-                if model.isUserAuthenticated {
-                    UniversalHeaderView {
-                        Image(.subscriptionActiveIcon)
-                            .padding(4)
-                    } content: {
-                        TextMenuItemHeader(UserText.preferencesSubscriptionActiveHeader)
-                        TextMenuItemCaption(model.subscriptionDetails ?? "")
-                    } buttons: {
-                        Button(UserText.addToAnotherDeviceButton) {
-                            model.userEventHandler(.addToAnotherDeviceClick)
-                            showingSheet.toggle()
-                        }
-
-                        Menu {
-                            Button(UserText.changePlanOrBillingButton, action: {
-                                model.userEventHandler(.changePlanOrBillingClick)
-                                Task {
-                                    switch await model.changePlanOrBillingAction() {
-                                    case .presentSheet(let sheet):
-                                        manageSubscriptionSheet = sheet
-                                    case .navigateToManageSubscription(let navigationAction):
-                                        navigationAction()
-                                    }
-                                }
-                            })
-                            Button(UserText.removeFromThisDeviceButton, action: {
-                                model.userEventHandler(.removeSubscriptionClick)
-                                showingRemoveConfirmationDialog.toggle()
-                            })
-                        } label: {
-                            Text(UserText.manageSubscriptionButton)
-                        }
-                        .fixedSize()
-                    }
-                    .onAppear {
-                        model.fetchAndUpdateSubscriptionDetails()
-                    }
-
-                } else {
-                    UniversalHeaderView {
-                        Image(.privacyPro)
-                            .padding(4)
-                            .background(Color("BadgeBackground", bundle: .module))
-                            .cornerRadius(4)
-                    } content: {
-                        TextMenuItemHeader(UserText.preferencesSubscriptionInactiveHeader)
-                        TextMenuItemCaption(UserText.preferencesSubscriptionInactiveCaption)
-                    } buttons: {
-                        Button(UserText.purchaseButton) { model.purchaseAction() }
-                            .buttonStyle(DefaultActionButtonStyle(enabled: true))
-                        Button(UserText.haveSubscriptionButton) {
-                            showingSheet.toggle()
-                            model.userEventHandler(.iHaveASubscriptionClick)
-                        }
-                    }
+                switch state {
+                case .noSubscription:
+                    unauthenticatedHeaderView
+                case .subscriptionPendingActivation:
+                    pendingActivationHeaderView
+                case .subscriptionActive:
+                    authenticatedHeaderView
+                case .subscriptionExpired:
+                    expiredHeaderView
                 }
 
                 Divider()
                     .foregroundColor(Color.secondary)
                     .padding(.horizontal, -10)
 
-                SectionView(iconName: "VPN-Icon",
-                            title: UserText.vpnServiceTitle,
-                            description: UserText.vpnServiceDescription,
-                            buttonName: model.isUserAuthenticated ? UserText.vpnServiceButtonTitle : nil,
-                            buttonAction: { model.openVPN() },
-                            enabled: !model.isUserAuthenticated || model.hasAccessToVPN)
-
-                Divider()
-                    .foregroundColor(Color.secondary)
-
-                SectionView(iconName: "PIR-Icon",
-                            title: UserText.personalInformationRemovalServiceTitle,
-                            description: UserText.personalInformationRemovalServiceDescription,
-                            buttonName: model.isUserAuthenticated ? UserText.personalInformationRemovalServiceButtonTitle : nil,
-                            buttonAction: { model.openPersonalInformationRemoval() },
-                            enabled: !model.isUserAuthenticated || model.hasAccessToDBP)
-
-                Divider()
-                    .foregroundColor(Color.secondary)
-
-                SectionView(iconName: "ITR-Icon",
-                            title: UserText.identityTheftRestorationServiceTitle,
-                            description: UserText.identityTheftRestorationServiceDescription,
-                            buttonName: model.isUserAuthenticated ? UserText.identityTheftRestorationServiceButtonTitle : nil,
-                            buttonAction: { model.openIdentityTheftRestoration() },
-                            enabled: !model.isUserAuthenticated || model.hasAccessToITR)
+                if state == .subscriptionActive {
+                    servicesRowsForActiveSubscriptionView
+                } else {
+                    servicesRowsForNoSubscriptionView
+                }
             }
             .padding(10)
             .roundedBorder()
@@ -175,19 +84,226 @@ public struct PreferencesSubscriptionView: View {
             Spacer()
                 .frame(height: 24)
 
-            PreferencePaneSection {
-                TextMenuItemHeader(UserText.preferencesSubscriptionFooterTitle)
-                HStack(alignment: .top, spacing: 6) {
-                    TextMenuItemCaption(UserText.preferencesSubscriptionFooterCaption)
-                    Button(UserText.viewFaqsButton) { model.openFAQ() }
-                }
-            }
+            footerView
         }
         .onAppear(perform: {
             if model.isUserAuthenticated {
                 model.userEventHandler(.activeSubscriptionSettingsClick)
             }
         })
+        .onReceive(model.statePublisher, perform: updateState(state:))
+    }
+
+    private func updateState(state: PreferencesSubscriptionState) {
+        self.state = state
+    }
+
+    @ViewBuilder
+    private var authenticatedHeaderView: some View {
+        UniversalHeaderView {
+            Image(.subscriptionActiveIcon)
+                .padding(4)
+        } content: {
+            TextMenuItemHeader(UserText.preferencesSubscriptionActiveHeader)
+            TextMenuItemCaption(model.subscriptionDetails ?? "")
+        } buttons: {
+            Button(UserText.addToAnotherDeviceButton) {
+                model.userEventHandler(.addToAnotherDeviceClick)
+                showingSheet.toggle()
+            }
+
+            Menu {
+                Button(UserText.changePlanOrBillingButton, action: {
+                    model.userEventHandler(.changePlanOrBillingClick)
+                    Task {
+                        switch await model.changePlanOrBillingAction() {
+                        case .presentSheet(let sheet):
+                            manageSubscriptionSheet = sheet
+                        case .navigateToManageSubscription(let navigationAction):
+                            navigationAction()
+                        }
+                    }
+                })
+                Button(UserText.removeFromThisDeviceButton, action: {
+                    model.userEventHandler(.removeSubscriptionClick)
+                    showingRemoveConfirmationDialog.toggle()
+                })
+            } label: {
+                Text(UserText.manageSubscriptionButton)
+            }
+            .fixedSize()
+        }
+        .onAppear {
+            model.fetchAndUpdateSubscriptionDetails()
+        }
+    }
+
+    @ViewBuilder
+    private var unauthenticatedHeaderView: some View {
+        UniversalHeaderView {
+            Image(.privacyPro)
+                .padding(4)
+                .background(Color("BadgeBackground", bundle: .module))
+                .cornerRadius(4)
+        } content: {
+            TextMenuItemHeader(UserText.preferencesSubscriptionInactiveHeader)
+            TextMenuItemCaption(UserText.preferencesSubscriptionInactiveCaption)
+        } buttons: {
+            Button(UserText.purchaseButton) { model.purchaseAction() }
+                .buttonStyle(DefaultActionButtonStyle(enabled: true))
+            Button(UserText.haveSubscriptionButton) {
+                showingSheet.toggle()
+                model.userEventHandler(.iHaveASubscriptionClick)
+            }
+            .buttonStyle(DismissActionButtonStyle())
+        }
+    }
+
+    @ViewBuilder
+    private var pendingActivationHeaderView: some View {
+        UniversalHeaderView {
+            Image(.subscriptionPendingIcon)
+                .padding(4)
+        } content: {
+            TextMenuItemHeader(UserText.preferencesSubscriptionPendingHeader)
+            TextMenuItemCaption(UserText.preferencesSubscriptionPendingCaption)
+        } buttons: {
+            Button(UserText.restorePurchaseButton) { model.refreshSubscriptionPendingState() }
+                .buttonStyle(DefaultActionButtonStyle(enabled: true))
+        }
+        .onAppear {
+            model.fetchAndUpdateSubscriptionDetails()
+        }
+    }
+
+    @ViewBuilder
+    private var expiredHeaderView: some View {
+        UniversalHeaderView {
+            Image(.subscriptionExpiredIcon)
+                .padding(4)
+        } content: {
+            TextMenuItemHeader(model.subscriptionDetails ?? UserText.preferencesSubscriptionInactiveHeader)
+            TextMenuItemCaption(UserText.preferencesSubscriptionExpiredCaption)
+        } buttons: {
+            Button(UserText.viewPlansButtonTitle) { model.purchaseAction() }
+                .buttonStyle(DefaultActionButtonStyle(enabled: true))
+            Menu {
+                Button(UserText.removeFromThisDeviceButton, action: {
+                    model.userEventHandler(.removeSubscriptionClick)
+                    showingRemoveConfirmationDialog.toggle()
+                })
+            } label: {
+                Text(UserText.manageDevicesButton)
+            }
+            .fixedSize()
+        }
+        .onAppear {
+            model.fetchAndUpdateSubscriptionDetails()
+        }
+    }
+
+    @ViewBuilder
+    private var servicesRowsForNoSubscriptionView: some View {
+        SectionView(iconName: "VPN-Icon",
+                    title: UserText.vpnServiceTitle,
+                    description: UserText.vpnServiceDescription)
+
+        Divider()
+            .foregroundColor(Color.secondary)
+
+        SectionView(iconName: "PIR-Icon",
+                    title: UserText.personalInformationRemovalServiceTitle,
+                    description: UserText.personalInformationRemovalServiceDescription)
+
+        Divider()
+            .foregroundColor(Color.secondary)
+
+        SectionView(iconName: "ITR-Icon",
+                    title: UserText.identityTheftRestorationServiceTitle,
+                    description: UserText.identityTheftRestorationServiceDescription)
+    }
+
+    @ViewBuilder
+    private var servicesRowsForActiveSubscriptionView: some View {
+        SectionView(iconName: "VPN-Icon",
+                    title: UserText.vpnServiceTitle,
+                    description: UserText.vpnServiceDescription,
+                    buttonName: UserText.vpnServiceButtonTitle,
+                    buttonAction: { model.openVPN() },
+                    enabled: model.hasAccessToVPN)
+
+        Divider()
+            .foregroundColor(Color.secondary)
+
+        SectionView(iconName: "PIR-Icon",
+                    title: UserText.personalInformationRemovalServiceTitle,
+                    description: UserText.personalInformationRemovalServiceDescription,
+                    buttonName: UserText.personalInformationRemovalServiceButtonTitle,
+                    buttonAction: { model.openPersonalInformationRemoval() },
+                    enabled: model.hasAccessToDBP)
+
+        Divider()
+            .foregroundColor(Color.secondary)
+
+        SectionView(iconName: "ITR-Icon",
+                    title: UserText.identityTheftRestorationServiceTitle,
+                    description: UserText.identityTheftRestorationServiceDescription,
+                    buttonName: UserText.identityTheftRestorationServiceButtonTitle,
+                    buttonAction: { model.openIdentityTheftRestoration() },
+                    enabled: model.hasAccessToITR)
+    }
+
+    @ViewBuilder
+    private var footerView: some View {
+        PreferencePaneSection {
+            TextMenuItemHeader(UserText.preferencesSubscriptionFooterTitle)
+            HStack(alignment: .top, spacing: 6) {
+                TextMenuItemCaption(UserText.preferencesSubscriptionFooterCaption)
+                Button(UserText.viewFaqsButton) { model.openFAQ() }
+            }
+        }
+    }
+
+    @ViewBuilder
+    private var removeConfirmationDialog: some View {
+        SubscriptionDialog(imageName: "Privacy-Pro-128",
+                           title: UserText.removeSubscriptionDialogTitle,
+                           description: UserText.removeSubscriptionDialogDescription,
+                           buttons: {
+            Button(UserText.removeSubscriptionDialogCancel) { showingRemoveConfirmationDialog = false }
+            Button(action: {
+                showingRemoveConfirmationDialog = false
+                model.removeFromThisDeviceAction()
+            }, label: {
+                Text(UserText.removeSubscriptionDialogConfirm)
+                    .foregroundColor(.red)
+            })
+        })
+        .frame(width: 320)
+    }
+
+    @ViewBuilder
+    private var manageSubscriptionAppStoreDialog: some View {
+        SubscriptionDialog(imageName: "app-store",
+                           title: UserText.changeSubscriptionDialogTitle,
+                           description: UserText.changeSubscriptionAppleDialogDescription,
+                           buttons: {
+            Button(UserText.changeSubscriptionDialogDone) { manageSubscriptionSheet = nil }
+                .buttonStyle(DefaultActionButtonStyle(enabled: true))
+        })
+        .frame(width: 360)
+    }
+
+    @ViewBuilder
+    private var manageSubscriptionGooglePlayDialog: some View {
+        SubscriptionDialog(imageName: "google-play",
+                           title: UserText.changeSubscriptionDialogTitle,
+                           description: UserText.changeSubscriptionGoogleDialogDescription,
+                           buttons: {
+            Button(UserText.changeSubscriptionDialogDone) { manageSubscriptionSheet = nil }
+                .buttonStyle(DefaultActionButtonStyle(enabled: true))
+        })
+        .frame(width: 360)
     }
 }
 
diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/Contents.json
new file mode 100644
index 0000000000..0feb43913c
--- /dev/null
+++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+  "images" : [
+    {
+      "filename" : "subscription-expired-icon.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/subscription-expired-icon.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-expired-icon.imageset/subscription-expired-icon.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..d43daeb9b999065914b2ae55c80a3467fee00e9a
GIT binary patch
literal 2374
zcmbVOOHbS|5Wf3Y%moQ5tz#!~oJa_%ETyX2s&wfM^^j%_Y{e$+CRDV)zGKH88y2aL
z9MJ6NnQtB*&v0;YcD|5YGbTa6@Z~E5I6j7x6K49g{mJqa&)%8-X1av}!Ijk-r=4lH
zEITtlHl10$K859F`l}n+cP6=TABZ3<NJ`FBQ1U{zWrEjQsJceWqEw=>LO!dFUC*Qn
zgqnM!Z2}g&P`a*4kdjwrE{iHgKn4^^$4H+r684XXpu;GAe$fx<KNDhvIu<P=nn)CP
z#1g-^?3(rL<KI?T^V}5)6js$rfEA|t?+dQNyqHxLubeel6T%uTq^<#N8)bzmxDec#
ziHO)Tm7=WkJYs^&nN)Gllpx-pwewmc#Bi5lxo#{evD0l(41{34L!^XJHrQD+Q-V+o
zgi#&t*#DMaQXYS<KjpB{p$hf_q;2OCA}B-uFDkC<274VZ#Xkvz{Ds;41BfNB1ilW`
z3v85I^h%JuhQd83SP)1>M98y6DDj$tKDx+8iAApjb^lsq@<f80B4vf2#vYM{&Xuxb
zpv1w_fbH>`o-d7)Eh|yA6hcFMdGUovURSDVY>oZPCl1$Q^MoP_686;zaiicVYxEr{
zD8|$!fjCbBNtEc6Od_$ZFx86A?c@zJE$5}Ib9~$dSGCmm)MOP7$sj_Q2w6llk)Amv
z8Z;*E=#DHXC^8_#PH!I`VI=Gy5rONl7jBH&mFM?aSblcT%}Y`0qM)5v5{$btd)fEH
zG{V(q+@1JkpMHKbkX^Jl#slxodefc`4{#Ot55*P}j#6M+nQ^$^b!G&+omZZ2IrNk1
z(W70>0la8;hyk{?gOUYqAzJL&+szt71)ISNf}iDZkG66z{t=Oc3D;-BWD^|8VN874
zPVL=r^VsxZXRet5&E~GMf1y&UP=K)iqyQ&DPf&qz+K(XYM7)zHP>F#(f^s37JqVv`
z(3*Db=FaSxy}dVQSdPPS*qS3a+^mo0qGYRKn7}6N3Y=YTei@A3{m0YpCYMewRn86$
IUVS+K4Zq#t=Kufz

literal 0
HcmV?d00001

diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Contents.json b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Contents.json
new file mode 100644
index 0000000000..d161417381
--- /dev/null
+++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+  "images" : [
+    {
+      "filename" : "Info-Color-16.pdf",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Info-Color-16.pdf b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Resources/Subscription.xcassets/Icons/subscription-pending-icon.imageset/Info-Color-16.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..42f3cc015dc1fe46421444dd220b6daad5be740b
GIT binary patch
literal 1745
zcma)7O^?(#5WV|X)MX?<!g1N<F9{*R%!1G^VraM_4oxNujb=Jar&+Q5dY&_h6KD?j
z(5il}D%X21SI#ako;_q8m0~i~fB3A39zCMRk5#w7Du0z-<IO+a{`&Zd9KcQS8jgo<
zd#jp@?!R5zZJs}+^Ec~%?V!FW)@EE}X5(SCPWS3od#j9g<3gn5WL$=kz0JZjw0o82
zd{Ye5tzxT%lZ|OUjfuU^<07Yh;%My(sySM<_$^6RR+vw#AzYzWEvYM5nJh^vRCBZ-
zImuH~RHO2$?iFd(C-w1x+N;-ps&LR=QnCpu1d$j$LE)<ZsZkU*I=aj*P}JThiwh{1
zGGR$GTPK-Vb7qc?l8$Vw@j$YZBxylfWwnde8Cfu^QBMfPs}$szDiU=vft{feH+e;E
zvyZ{U%R3v9DQHAT2{<nr_AV_5u2Wmd-tdCe$^%kHLBSQN{EDsy`4h7tuRV*40jyDd
zkZ1~)Ob*%7&2KR9w(O%oV;ml&C=ruLvMAd5<i&y!;{u?e!KQ>LQjFv{cr>Oq33Q7R
z0Sr^wvTOv-MTy|pT0z;tc@7R<;*;@yYIboYr37o@f|L&P!C2hlUH8DN_8hhtGS6G!
zR}z3>sk3^a_RJn^jOKtYC_RK%2oQ*~#z0EZ98Ct%5+{Zu?p1WKW1T=iDtoAB@cv+&
zYP{;`GJU4eGS8tha_;66E=KBjf)>lzYM$)({c)go?=i-(nz#S|+fj45z3wL9Wp}mP
zKJCBJyR`(<`&*v#V$GR0-OztMwB0~uP;RDl=l%ZJ?Qvq6k~4U@Jpdz69HG@LEl{1Y
z&ELB#luCuchURn5`>%-QllliBt4z4BRi;94s0S{@&$q|zO@IA!=(j`nQ5lNrj&=E^
z;Jxz%cv@)v(IQNMX*twcR)v`J31k_4_p?`N^IsqdOW^Ua-Q9Etg^y12;sSEq8~WSs
qKHb}0-M<s1+4TL93gJ=U=FRSVhw8_V&-QQ}>9Ds>ot^#l>e&xM_ijW0

literal 0
HcmV?d00001

diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift
index 41063cca08..3e14765641 100644
--- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift
+++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift
@@ -17,12 +17,13 @@
 //
 
 import Foundation
+import Subscription
 
 public final class ActivateSubscriptionAccessModel: SubscriptionAccessModel, PurchaseRestoringSubscriptionAccessModel {
     private var actionHandlers: SubscriptionAccessActionHandlers
 
     public var title = UserText.activateModalTitle
-    public var description = UserText.activateModalDescription
+    public var description = UserText.activateModalDescription(platform: SubscriptionPurchaseEnvironment.current)
 
     public var email: String?
     public var emailLabel: String { UserText.email }
@@ -31,7 +32,7 @@ public final class ActivateSubscriptionAccessModel: SubscriptionAccessModel, Pur
 
     public private(set) var shouldShowRestorePurchase: Bool
     public var restorePurchaseDescription = UserText.restorePurchaseDescription
-    public var restorePurchaseButtonTitle = UserText.restorePurchasesButton
+    public var restorePurchaseButtonTitle = UserText.restorePurchaseButton
 
     public init(actionHandlers: SubscriptionAccessActionHandlers, shouldShowRestorePurchase: Bool) {
         self.actionHandlers = actionHandlers
diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift
index 5ebc6d5433..9e007f4e7e 100644
--- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift
+++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift
@@ -21,7 +21,7 @@ import Subscription
 
 public final class ShareSubscriptionAccessModel: SubscriptionAccessModel {
     public var title = UserText.shareModalTitle
-    public var description = UserText.shareModalDescription
+    public var description = UserText.shareModalDescription(platform: SubscriptionPurchaseEnvironment.current)
 
     private let subscriptionAppGroup: String
     private var actionHandlers: SubscriptionAccessActionHandlers
diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift
index 8be8b6f333..bf1931a6fc 100644
--- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift
+++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessView.swift
@@ -115,6 +115,7 @@ public struct SubscriptionAccessView: View {
                 Button("Cancel") {
                     dismiss()
                 }
+                .buttonStyle(DismissActionButtonStyle())
             }
             .padding(.horizontal, 20)
             .padding(.vertical, 16)
diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift
index 05c1ff1a5f..f90aba85ad 100644
--- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift
+++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift
@@ -17,6 +17,7 @@
 //
 
 import Foundation
+import Subscription
 
 enum UserText {
     // MARK: - Subscription preferences
@@ -42,11 +43,25 @@ enum UserText {
 
     // MARK: Preferences when subscription is active
     static let preferencesSubscriptionActiveHeader = NSLocalizedString("subscription.preferences.subscription.active.header", value: "Privacy Pro is active on this device", comment: "Header for the subscription preferences pane when the subscription is active")
-    static func preferencesSubscriptionActiveCaption(formattedDate: String) -> String {
-        let localized = NSLocalizedString("subscription.preferences.subscription.active.caption", value: "Your monthly Privacy Pro subscription renews on %@", comment: "Caption for the subscription preferences pane when the subscription is active")
+
+    static func preferencesSubscriptionActiveRenewCaption(period: String, formattedDate: String) -> String {
+        let localized = NSLocalizedString("subscription.preferences.subscription.active.renew.caption", value: "Your %@ Privacy Pro subscription renews on %@.", comment: "Caption for the subscription preferences pane when the subscription is active and will renew. First parameter is renewal period (monthly/yearly). Second parameter is date.")
+        return String(format: localized, period, formattedDate)
+    }
+
+    static func preferencesSubscriptionActiveExpireCaption(period: String, formattedDate: String) -> String {
+        let localized = NSLocalizedString("subscription.preferences.subscription.active.expire.caption", value: "Your %@ Privacy Pro subscription expires on %@.", comment: "Caption for the subscription preferences pane when the subscription is active but will expire. First parameter is renewal period (monthly/yearly). Second parameter is date.")
+        return String(format: localized, period, formattedDate)
+    }
+
+    static func preferencesSubscriptionExpiredCaption(formattedDate: String) -> String {
+        let localized = NSLocalizedString("subscription.preferences.subscription.expired.caption", value: "Your Privacy Pro subscription expired on %@.", comment: "Caption for the subscription preferences pane when the subscription has expired. The parameter is date of expiry.")
         return String(format: localized, formattedDate)
     }
 
+    static let monthlySubscriptionBillingPeriod = NSLocalizedString("subscription.billing.period.monthly", value: "Monthly", comment: "Type of subscription billing period that lasts a month")
+    static let yearlySubscriptionBillingPeriod = NSLocalizedString("subscription.billing.period.yearly", value: "Yearly", comment: "Type of subscription billing period that lasts a year")
+
     static let addToAnotherDeviceButton = NSLocalizedString("subscription.preferences.add.to.another.device.button", value: "Add to Another Device…", comment: "Button to add subscription to another device")
     static let manageSubscriptionButton = NSLocalizedString("subscription.preferences.manage.subscription.button", value: "Manage Subscription", comment: "Button to manage subscription")
     static let changePlanOrBillingButton = NSLocalizedString("subscription.preferences.change.plan.or.billing.button", value: "Change Plan or Billing...", comment: "Button to add subscription to another device")
@@ -59,6 +74,15 @@ enum UserText {
     static let purchaseButton = NSLocalizedString("subscription.preferences.purchase.button", value: "Get Privacy Pro", comment: "Button to open a page where user can learn more and purchase the subscription")
     static let haveSubscriptionButton = NSLocalizedString("subscription.preferences.i.have.a.subscription.button", value: "I Have a Subscription", comment: "Button enabling user to activate a subscription user bought earlier or on another device")
 
+    // MARK: Preferences when subscription activation is pending
+    static let preferencesSubscriptionPendingHeader = NSLocalizedString("subscription.preferences.subscription.pending.header", value: "Your Subscription is Being Activated", comment: "Header for the subscription preferences pane when the subscription activation is pending")
+    static let preferencesSubscriptionPendingCaption = NSLocalizedString("subscription.preferences.subscription.pending.caption", value: "This is taking longer than usual, please check back later.", comment: "Caption for the subscription preferences pane when the subscription activation is pending")
+
+    // MARK: Preferences when subscription is expired
+    static let preferencesSubscriptionExpiredCaption = NSLocalizedString("subscription.preferences.subscription.expired.caption", value: "Subscribe again to continue using Privacy Pro.", comment: "Caption for the subscription preferences pane when the subscription activation is pending")
+
+    static let manageDevicesButton = NSLocalizedString("subscription.preferences.manage.devices.button", value: "Manage Devices", comment: "Button to manage devices")
+
     // MARK: - Change plan or billing dialogs
     static let changeSubscriptionDialogTitle = NSLocalizedString("subscription.dialog.change.title", value: "Change Plan or Billing", comment: "Change plan or billing dialog title")
     static let changeSubscriptionGoogleDialogDescription = NSLocalizedString("subscription.dialog.change.google.description", value: "Your subscription was purchased through the Google Play Store. To change your plan or billing settings, please open Google Play Store subscription settings on a device signed in to the same Google Account used to purchase your subscription.", comment: "Change plan or billing dialog subtitle description for subscription purchased via Google")
@@ -76,14 +100,28 @@ enum UserText {
 
     // MARK: - Activate subscription modal
     static let activateModalTitle = NSLocalizedString("subscription.activate.modal.title", value: "Activate your subscription on this device", comment: "Activate subscription modal view title")
-    static let activateModalDescription = NSLocalizedString("subscription.activate.modal.description", value: "Access your subscription on other devices via Apple ID or an 
email address.", comment: "Activate subscription modal view subtitle description")
+    static func activateModalDescription(platform: SubscriptionPurchaseEnvironment.Environment) -> String {
+        switch platform {
+        case .appStore:
+            NSLocalizedString("subscription.appstore.activate.modal.description", value: "Access your subscription on other devices via Apple ID or an email address.", comment: "Activate subscription modal view subtitle description")
+        case .stripe:
+            NSLocalizedString("subscription.activate.modal.description", value: "Access your subscription on other devices via an email address.", comment: "Activate subscription modal view subtitle description")
+        }
+    }
 
-    static let activateModalEmailDescription = NSLocalizedString("subscription.activate.modal.email.description", value: "Use your email to access your subscription on this device.", comment: "Activate subscription modal description for email address channel")
+    static let activateModalEmailDescription = NSLocalizedString("subscription.activate.modal.email.description", value: "Use your email to activate your subscription on this device.", comment: "Activate subscription modal description for email address channel")
     static let restorePurchaseDescription = NSLocalizedString("subscription.activate.modal.restore.purchase.description", value: "Your subscription is automatically available in DuckDuckGo on any device signed in to your Apple ID.", comment: "Activate subscription modal description via restore purchase from Apple ID")
 
     // MARK: - Share subscription modal
-    static let shareModalTitle = NSLocalizedString("subscription.share.modal.title", value: "Use your subscription on all your devices", comment: "Share subscription modal view title")
-    static let shareModalDescription = NSLocalizedString("subscription.share.modal.description", value: "Access your Privacy Pro subscription on any of your devices via Sync, Apple ID or by adding an email address.", comment: "Share subscription modal view subtitle description")
+    static let shareModalTitle = NSLocalizedString("subscription.share.modal.title", value: "Use your subscription on other devices", comment: "Share subscription modal view title")
+    static func shareModalDescription(platform: SubscriptionPurchaseEnvironment.Environment) -> String {
+        switch platform {
+        case .appStore:
+            NSLocalizedString("subscription.appstore.share.modal.description", value: "Access your subscription via Apple ID or by adding an email address.", comment: "Share subscription modal view subtitle description")
+        case .stripe:
+            NSLocalizedString("subscription.share.modal.description", value: "Activate your Privacy Pro subscription via an email address.", comment: "Share subscription modal view subtitle description")
+        }
+    }
 
     static let shareModalHasEmailDescription = NSLocalizedString("subscription.share.modal.has.email.description", value: "Use this email to activate your subscription on other devices. Open the DuckDuckGo app on another device and find Privacy Pro in browser settings.", comment: "Share subscription modal description for email address channel")
     static let shareModalNoEmailDescription = NSLocalizedString("subscription.share.modal.no.email.description", value: "Add an email address to access your subscription in DuckDuckGo on other devices. We’ll only use this address to verify your subscription.", comment: "Share subscription modal description for email address channel")
@@ -91,7 +129,7 @@ enum UserText {
     static let restorePurchasesDescription = NSLocalizedString("subscription.share.modal.restore.purchases.description", value: "Your subscription is automatically available in DuckDuckGo on any device signed in to your Apple ID.", comment: "Share subscription modal description for restoring Apple ID purchases")
 
     // MARK: - Activate/share modal buttons
-    static let restorePurchasesButton = NSLocalizedString("subscription.modal.restore.purchases.button", value: "Restore Purchases", comment: "Button for restoring past subscription purchases")
+    static let restorePurchaseButton = NSLocalizedString("subscription.modal.restore.purchase.button", value: "Restore Purchase", comment: "Button for restoring past subscription purchase")
     static let manageEmailButton = NSLocalizedString("subscription.modal.manage.email.button", value: "Manage Email", comment: "Button for opening manage email address page")
     static let enterEmailButton = NSLocalizedString("subscription.modal.enter.email.button", value: "Enter Email", comment: "Button for opening page to enter email address")
     static let addEmailButton = NSLocalizedString("subscription.modal.add.email.button", value: "Add Email", comment: "Button for opening page to add email address")
@@ -103,8 +141,7 @@ enum UserText {
     static let restoreButtonTitle = NSLocalizedString("subscription.alert.button.restore", value: "Restore", comment: "Alert button for restoring past subscription purchases")
 
     static let somethingWentWrongAlertTitle = NSLocalizedString("subscription.alert.something.went.wrong.title", value: "Something Went Wrong", comment: "Alert title when unknown error has occurred")
-    static let somethingWentWrongAlertDescription = NSLocalizedString("subscription.alert.something.went.wrong.description", value: "The App Store was not able to process your purchase. Please try again later.", comment: "Alert message when unknown error has occurred")
-    static let somethingWentWrongStripeAlertDescription = NSLocalizedString("subscription.alert.something.went.wrong.stripe.description", value: "We were not able to start your purchase process. Please try again later.", comment: "Alert message when unknown error has occurred")
+    static let somethingWentWrongAlertDescription = NSLocalizedString("subscription.alert.something.went.wrong.description", value: "We’re having trouble connecting. Please try again later.", comment: "Alert message when unknown error has occurred")
 
     static let subscriptionNotFoundAlertTitle = NSLocalizedString("subscription.alert.subscription.not.found.title", value: "Subscription Not Found", comment: "Alert title when subscription was not found")
     static let subscriptionNotFoundAlertDescription = NSLocalizedString("subscription.alert.subscription.not.found.description", value: "We couldn’t find a subscription associated with this Apple ID.", comment: "Alert message when subscription was not found")

From 4167e4d43f9efae33ff11ba39970751e3ba8e761 Mon Sep 17 00:00:00 2001
From: bwaresiak <bartek@duckduckgo.com>
Date: Fri, 15 Mar 2024 13:38:32 +0100
Subject: [PATCH 05/16] Stub objects for Bookmarks DB (#2418)

Task/Issue URL: https://app.asana.com/0/414235014887631/1206754257727808/f

Description:

Provide implementation for stub objects.

Steps to test this PR:
See BSK PR.
---
 DuckDuckGo.xcodeproj/project.pbxproj          |  2 +-
 .../xcshareddata/swiftpm/Package.resolved     |  6 ++---
 DuckDuckGo/Bookmarks/Model/Bookmark.swift     | 25 ++++++++++++++-----
 DuckDuckGo/Sync/SyncDebugMenu.swift           | 25 +++++++++++++++++++
 .../DataBrokerProtection/Package.swift        |  2 +-
 .../NetworkProtectionMac/Package.swift        |  2 +-
 LocalPackages/SubscriptionUI/Package.swift    |  2 +-
 7 files changed, 51 insertions(+), 13 deletions(-)

diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj
index a809af52f6..76a2d243ff 100644
--- a/DuckDuckGo.xcodeproj/project.pbxproj
+++ b/DuckDuckGo.xcodeproj/project.pbxproj
@@ -13749,7 +13749,7 @@
 			repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit";
 			requirement = {
 				kind = exactVersion;
-				version = 123.0.1;
+				version = 124.0.0;
 			};
 		};
 		AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 7f8548488a..e8258208b3 100644
--- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -32,8 +32,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/duckduckgo/BrowserServicesKit",
       "state" : {
-        "revision" : "838cb53a8f7050d87ae6931b45ce126ece994359",
-        "version" : "123.0.1"
+        "branch" : "bartek/stub-objects",
+        "revision" : "968c429c464688c641d100eab584aa95b039e371"
       }
     },
     {
@@ -165,7 +165,7 @@
     {
       "identity" : "trackerradarkit",
       "kind" : "remoteSourceControl",
-      "location" : "https://github.com/duckduckgo/TrackerRadarKit",
+      "location" : "https://github.com/duckduckgo/TrackerRadarKit.git",
       "state" : {
         "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff",
         "version" : "1.2.2"
diff --git a/DuckDuckGo/Bookmarks/Model/Bookmark.swift b/DuckDuckGo/Bookmarks/Model/Bookmark.swift
index 99ca656b3f..c7c5f61076 100644
--- a/DuckDuckGo/Bookmarks/Model/Bookmark.swift
+++ b/DuckDuckGo/Bookmarks/Model/Bookmark.swift
@@ -23,25 +23,32 @@ internal class BaseBookmarkEntity: Identifiable {
 
     static func singleEntity(with uuid: String) -> NSFetchRequest<BookmarkEntity> {
         let request = BookmarkEntity.fetchRequest()
-        request.predicate = NSPredicate(format: "%K == %@ AND %K == NO", #keyPath(BookmarkEntity.uuid), uuid, #keyPath(BookmarkEntity.isPendingDeletion))
+        request.predicate = NSPredicate(format: "%K == %@ AND %K == NO AND (%K == NO OR %K == nil)",
+                                        #keyPath(BookmarkEntity.uuid), uuid,
+                                        #keyPath(BookmarkEntity.isPendingDeletion),
+                                        #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub))
         return request
     }
 
     static func favorite(with uuid: String, favoritesFolder: BookmarkEntity) -> NSFetchRequest<BookmarkEntity> {
         let request = BookmarkEntity.fetchRequest()
-        request.predicate = NSPredicate(format: "%K == %@ AND %K CONTAINS %@ AND %K == NO AND %K == NO",
+        request.predicate = NSPredicate(format: "%K == %@ AND %K CONTAINS %@ AND %K == NO AND %K == NO AND (%K == NO OR %K == nil)",
                                         #keyPath(BookmarkEntity.uuid),
                                         uuid as CVarArg,
                                         #keyPath(BookmarkEntity.favoriteFolders),
                                         favoritesFolder,
                                         #keyPath(BookmarkEntity.isFolder),
-                                        #keyPath(BookmarkEntity.isPendingDeletion))
+                                        #keyPath(BookmarkEntity.isPendingDeletion),
+                                        #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub))
         return request
     }
 
     static func entities(with identifiers: [String]) -> NSFetchRequest<BookmarkEntity> {
         let request = BookmarkEntity.fetchRequest()
-        request.predicate = NSPredicate(format: "%K IN %@ AND %K == NO", #keyPath(BookmarkEntity.uuid), identifiers, #keyPath(BookmarkEntity.isPendingDeletion))
+        request.predicate = NSPredicate(format: "%K IN %@ AND %K == NO AND (%K == NO OR %K == nil)",
+                                        #keyPath(BookmarkEntity.uuid), identifiers,
+                                        #keyPath(BookmarkEntity.isPendingDeletion),
+                                        #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub))
         return request
     }
 
@@ -98,7 +105,10 @@ final class BookmarkFolder: BaseBookmarkEntity {
 
     static func bookmarkFoldersFetchRequest() -> NSFetchRequest<BookmarkEntity> {
         let request = BookmarkEntity.fetchRequest()
-        request.predicate = NSPredicate(format: "%K == YES AND %K == NO", #keyPath(BookmarkEntity.isFolder), #keyPath(BookmarkEntity.isPendingDeletion))
+        request.predicate = NSPredicate(format: "%K == YES AND %K == NO AND (%K == NO OR %K == nil)",
+                                        #keyPath(BookmarkEntity.isFolder),
+                                        #keyPath(BookmarkEntity.isPendingDeletion),
+                                        #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub))
         return request
     }
 
@@ -135,7 +145,10 @@ final class Bookmark: BaseBookmarkEntity {
 
     static func bookmarksFetchRequest() -> NSFetchRequest<BookmarkEntity> {
         let request = BookmarkEntity.fetchRequest()
-        request.predicate = NSPredicate(format: "%K == NO AND %K == NO", #keyPath(BookmarkEntity.isFolder), #keyPath(BookmarkEntity.isPendingDeletion))
+        request.predicate = NSPredicate(format: "%K == NO AND %K == NO AND (%K == NO OR %K == nil)",
+                                        #keyPath(BookmarkEntity.isFolder),
+                                        #keyPath(BookmarkEntity.isPendingDeletion),
+                                        #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub))
         return request
     }
 
diff --git a/DuckDuckGo/Sync/SyncDebugMenu.swift b/DuckDuckGo/Sync/SyncDebugMenu.swift
index 6d60705595..da1fe3e0be 100644
--- a/DuckDuckGo/Sync/SyncDebugMenu.swift
+++ b/DuckDuckGo/Sync/SyncDebugMenu.swift
@@ -18,6 +18,7 @@
 
 import Foundation
 import DDGSync
+import Bookmarks
 
 @MainActor
 final class SyncDebugMenu: NSMenu {
@@ -32,6 +33,8 @@ final class SyncDebugMenu: NSMenu {
                 .submenu(environmentMenu)
             NSMenuItem(title: "Reset Favicons Fetcher Onboarding Dialog", action: #selector(resetFaviconsFetcherOnboardingDialog))
                 .targetting(self)
+            NSMenuItem(title: "Populate Stub objects", action: #selector(createStubsForDebug))
+                .targetting(self)
         }
     }
 
@@ -78,6 +81,28 @@ final class SyncDebugMenu: NSMenu {
 #endif
     }
 
+    @objc func createStubsForDebug() {
+#if DEBUG || REVIEW
+        let db = BookmarkDatabase.shared
+
+        let context = db.db.makeContext(concurrencyType: .privateQueueConcurrencyType)
+
+        context.performAndWait {
+            let root = BookmarkUtils.fetchRootFolder(context)!
+
+            _ = BookmarkEntity.makeBookmark(title: "Non stub", url: "url", parent: root, context: context)
+            let stub = BookmarkEntity.makeBookmark(title: "Stub", url: "", parent: root, context: context)
+            stub.isStub = true
+            let emptyStub = BookmarkEntity.makeBookmark(title: "", url: "", parent: root, context: context)
+            emptyStub.isStub = true
+            emptyStub.title = nil
+            emptyStub.url = nil
+
+            try? context.save()
+        }
+#endif
+    }
+
     @objc func resetFaviconsFetcherOnboardingDialog(_ sender: NSMenuItem) {
         UserDefaults.standard.set(false, forKey: UserDefaultsWrapper<String>.Key.syncDidPresentFaviconsFetcherOnboarding.rawValue)
     }
diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift
index 8e88a31067..dd701eff21 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: "123.0.1"),
+        .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.0.0"),
         .package(path: "../PixelKit"),
         .package(path: "../SwiftUIExtensions"),
         .package(path: "../XPCHelper"),
diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift
index a5aa5f7c63..000dc4e3f9 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: "123.0.1"),
+        .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.0.0"),
         .package(path: "../XPCHelper"),
         .package(path: "../SwiftUIExtensions"),
         .package(path: "../LoginItems"),
diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift
index fb17968848..98ceb2ad67 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: "123.0.1"),
+        .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.0.0"),
         .package(path: "../SwiftUIExtensions")
     ],
     targets: [

From 409350dcc856c7707adffa55c476a69e1e3a655f Mon Sep 17 00:00:00 2001
From: Pete Smith <peadar81@gmail.com>
Date: Fri, 15 Mar 2024 12:56:28 +0000
Subject: [PATCH 06/16] Password Import: Ignore Excluded Sites When Importing
 from Chrome (#2404)

Task/Issue URL: https://app.asana.com/0/0/1206830207129428/f
CC:

Description:
This is a Fix/Improvement as part of macOS: Investigate & Action Top Failed Import Issues for Passwords.
---
 .../Logins/Chromium/ChromiumLoginReader.swift |   6 ++--
 .../DataImport/ChromiumLoginReaderTests.swift |  34 ++++++++++++++++++
 .../TestChromeData/Legacy Excluded/Login Data | Bin 0 -> 40960 bytes
 .../TestChromeData/Legacy/Login Data          | Bin 40960 -> 40960 bytes
 .../TestChromeData/v32 Excluded/Login Data    | Bin 0 -> 40960 bytes
 .../v32 Excluded/Login Data for Account       | Bin 0 -> 40960 bytes
 .../TestChromeData/v32/Login Data             | Bin 40960 -> 40960 bytes
 7 files changed, 37 insertions(+), 3 deletions(-)
 create mode 100644 UnitTests/DataImport/DataImportResources/TestChromeData/Legacy Excluded/Login Data
 create mode 100644 UnitTests/DataImport/DataImportResources/TestChromeData/v32 Excluded/Login Data
 create mode 100644 UnitTests/DataImport/DataImportResources/TestChromeData/v32 Excluded/Login Data for Account

diff --git a/DuckDuckGo/DataImport/Logins/Chromium/ChromiumLoginReader.swift b/DuckDuckGo/DataImport/Logins/Chromium/ChromiumLoginReader.swift
index 2c5b36ba8e..c423ec9025 100644
--- a/DuckDuckGo/DataImport/Logins/Chromium/ChromiumLoginReader.swift
+++ b/DuckDuckGo/DataImport/Logins/Chromium/ChromiumLoginReader.swift
@@ -65,9 +65,9 @@ final class ChromiumLoginReader {
     private let decryptionKey: String?
     private let decryptionKeyPrompt: ChromiumKeychainPrompting
 
-    private static let sqlSelectWithPasswordTimestamp = "SELECT signon_realm, username_value, password_value, date_password_modified FROM logins;"
-    private static let sqlSelectWithCreatedTimestamp = "SELECT signon_realm, username_value, password_value, date_created FROM logins;"
-    private static let sqlSelectWithoutTimestamp = "SELECT signon_realm, username_value, password_value FROM logins;"
+    private static let sqlSelectWithPasswordTimestamp = "SELECT signon_realm, username_value, password_value, date_password_modified, blacklisted_by_user FROM logins WHERE blacklisted_by_user != 1;"
+    private static let sqlSelectWithCreatedTimestamp = "SELECT signon_realm, username_value, password_value, date_created, blacklisted_by_user FROM logins WHERE blacklisted_by_user != 1;"
+    private static let sqlSelectWithoutTimestamp = "SELECT signon_realm, username_value, password_value, blacklisted_by_user FROM logins WHERE blacklisted_by_user != 1;"
 
     private let source: DataImport.Source
 
diff --git a/UnitTests/DataImport/ChromiumLoginReaderTests.swift b/UnitTests/DataImport/ChromiumLoginReaderTests.swift
index ba59c59bba..4c5d835e7a 100644
--- a/UnitTests/DataImport/ChromiumLoginReaderTests.swift
+++ b/UnitTests/DataImport/ChromiumLoginReaderTests.swift
@@ -22,7 +22,9 @@ import XCTest
 
 private struct ChromiumLoginStore {
     static let legacy: Self = .init(directory: "Legacy", decryptionKey: "0geUdf5dTuZmIrtd8Omf/Q==")
+    static let legacyExcluded: Self = .init(directory: "Legacy Excluded", decryptionKey: "0geUdf5dTuZmIrtd8Omf/Q==")
     static let v32: Self = .init(directory: "v32", decryptionKey: "IcBAbGhvYp70AP+5W5ojcw==")
+    static let v32Excluded: Self = .init(directory: "v32 Excluded", decryptionKey: "IcBAbGhvYp70AP+5W5ojcw==")
 
     let directory: String
     let decryptionKey: String
@@ -64,6 +66,22 @@ class ChromiumLoginReaderTests: XCTestCase {
         XCTAssertEqual(logins[2].password, "password")
     }
 
+    func testImportFromVersion32_WithOnlyExcludedSites_IgnoresExcludedCredentials() throws {
+        // Given
+        let expectedResult: DataImportResult<[ImportedLoginCredential]> = .success([])
+        let reader = ChromiumLoginReader(
+            chromiumDataDirectoryURL: ChromiumLoginStore.v32Excluded.databaseDirectoryURL,
+            source: .chrome,
+            decryptionKey: ChromiumLoginStore.v32Excluded.decryptionKey
+        )
+
+        // When
+        let actualResult = reader.readLogins()
+
+        // Then
+        XCTAssertEqual(expectedResult, actualResult)
+    }
+
     func testImportFromLegacyVersion() throws {
 
         let reader = ChromiumLoginReader(
@@ -83,6 +101,22 @@ class ChromiumLoginReaderTests: XCTestCase {
         XCTAssertEqual(logins[0].password, "password")
     }
 
+    func testImportFromLegacyVersion_WithOnlyExcludedSites_IgnoresExcludedCredentials() {
+        // Given
+        let expectedResult: DataImportResult<[ImportedLoginCredential]> = .success([])
+        let reader = ChromiumLoginReader(
+            chromiumDataDirectoryURL: ChromiumLoginStore.legacyExcluded.databaseDirectoryURL,
+            source: .chrome,
+            decryptionKey: ChromiumLoginStore.legacyExcluded.decryptionKey
+        )
+
+        // When
+        let actualResult = reader.readLogins()
+
+        // Then
+        XCTAssertEqual(expectedResult, actualResult)
+    }
+
     func testWhenImportingChromiumData_AndTheUserCancelsTheKeychainPrompt_ThenAnErrorIsReturned() {
         let mockPrompt = MockChromiumPrompt(returnValue: .userDeniedKeychainPrompt)
         let reader = ChromiumLoginReader(
diff --git a/UnitTests/DataImport/DataImportResources/TestChromeData/Legacy Excluded/Login Data b/UnitTests/DataImport/DataImportResources/TestChromeData/Legacy Excluded/Login Data
new file mode 100644
index 0000000000000000000000000000000000000000..0acb8dc848e3636e8d752fcf7b06458492928ab0
GIT binary patch
literal 40960
zcmeI)PjA~~90%~nO%`Vb1BH&NNJS4jK&zT|gBwVUbPJ}CrEN$zda@kH%d9$f=ErGP
z4sf8Hc0jNb>Wv$`fSz_5;>Lj!N5pLsmq~jA<@eZenzUWk6>XI1`$UgZKmYtZpXZ6}
z+HF2tDY=ouuLWV#iuk)M&lH7y$T?$7#&L?zQ+$!)D-^R7)1ah340-2l?&>F6)@Rww
zHp}j23z_?wZN=Yv4XOJKW~sr*+t1*zFr+6TP|4*c-kMZ|>(`x|UeIuTVG6h52Y#}n
zUaS-g)goUkUn+ji2MX||ayNlbCsoYQvApKY@~Pa!?5wgDSvAiQH$3|6o5H!#c6@vP
zZm!#WwXjes@_l8eeXHr{RC8uFo13^Wsj#@0-BMON3gUgU+pF`*`zd1j?nzJ`yUlOD
z%%~HSlgh2Du}IR*WSborf0D+hgV3dkFx#QWuM{d5KQ2^wd8x|FtEH08Ejx09flQsZ
zg%e6F)A5|9<40+=zMKuqYdd@CElY?_5Y`XY=;b6e7D`JCIv4F)(~S-m?w|Lx@xH#+
zEt-0pRz>90X$e(}MzMb=HP5owJy%e!S=%(_C=Mwl?CUhBLo#|jjW%1oe4R&b(-E?<
z-XZn))MHX_ES`)f?wHu51=&}q?uwRYZJJUt9gNOhd%yNIr|yK-!C4UNZp-|_2?Nvi
zTzft3vCbQg->cB_0;_I{$coxxuS6>l!lh43Y!J_6%W^}}?XBDEBwwy9UM^JD_@~7+
zURbRzEtY8oT`rcZI*-TXSt1%3dv7-QrQ%10)l!w8(|I%4aQ%i^^8$OF?CHeZJ<@7<
zab>mGzXJP~P<L_lrNm1_=e>#OmFQm=Gc(8j<e(x1AOHafKmY;|fB*y_009U<00RFT
z0WJ3`Q?j>N_BL%0AOL}<C@>N{#e@Avhf;P7<IL2UrWv`@`FwuM$!nUHr_RWz{9tHj
zrex2QZz<j~DDP{QdriJmFtq(uu18Tzyf;7J>2&5or(SoXIXh_1$G7L_hh|RG^Ga17
ziNaZmX<^^#h`CK_q~`io6oh;29lhKLwI_!ogM374F`HgdZ$ENw#yh*ayI;kL-Hm*N
znnu~i{vaQiq#<ozmTR$2oGjDQJG?UC-?T6LA^YyPJ&$_z_sR;*+B-CB3Wcq@Lerrr
z<19D+Xt+n0uxy)dvL{bQTAhva=N62+@BjGV^*@a5t>vFC-1~lL6Kwv?zka><AlAXO
z%84Hg{mW)>T9a~;@9r<Z%bij6XQrqPrZ#AU009U<00Izzz!3%3A6jl@YRr(QU&f3D
zRbJ5=N_tuI_?NFaYUYY;Fm2-D*R6vWS3}EZHT5o?|96<WL$BZB&SC=r2tWV=5P-mm
z6c`=JOe)Erq{{1p502l-QT0zc|KDNi9ois300Izz00ba#;sxHw?A;AG^oD}GHaI){
z_x}?=NOTYa5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwx`qYAu0
z1)rlhN^y*${JvkArcr!}qWnkG(-bol<v*ax?**3M2mCTc1PDL?0uX=z1Rwwb2tWV=
z5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHaf
kKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rhi1AJH3Z1ONa4

literal 0
HcmV?d00001

diff --git a/UnitTests/DataImport/DataImportResources/TestChromeData/Legacy/Login Data b/UnitTests/DataImport/DataImportResources/TestChromeData/Legacy/Login Data
index d1e638bf0cc38240ff3c0cd2beb7ab0b59c94af3..f59f562ef053406026e5f105cc3a6059ca0ebf87 100644
GIT binary patch
delta 410
zcmZoTz|?SnX@WGP;zSu|Rz(KAaIcLi3&c5?Snn~giLu_>%*av1Jh@K#Nj>8>cXv@)
zL0(>WHck!>j+RsoUS3`f1_lNYW}NBHD=G_QFfjH5ako2A4oEkHXdrOsMc0y1Qc_TC
zrLSLJUanV^nv#-PqL-YXtDlpfo|&hQO_CEXJNcm87Yj3XNvPSp9GoB*zyJpWHr;T|
zo5kebF>)}mw=l4;Vs8QZ&WwGsle&~&psb)fBiIagAn6QrG9y-VRN2ASVwZ%upMCQx
vb#V?>cA!ai6C1528L$9tyve|RlN}VEo7v@+u$m3h&ce*g$iDfWxl#rIC+TQo

delta 119
zcmZoTz|?SnX@WGP%tRSyRv8ApaF2~C3&c4X*~A#w#F%R~GjbF$Pp*@GvUvhyHzO01
z=)^|#&0=!z7&#c(S23`!Vz&pXFk_$Wq%Jjag5Ty<>f#)%EDQ_`b`u+|CK<2*Ro!D?
WzsG(IsA?}eqvS+Kk<Ital`;T<R3ME2

diff --git a/UnitTests/DataImport/DataImportResources/TestChromeData/v32 Excluded/Login Data b/UnitTests/DataImport/DataImportResources/TestChromeData/v32 Excluded/Login Data
new file mode 100644
index 0000000000000000000000000000000000000000..6a4ffefe50d8c8490d7a8071714c0442d4959781
GIT binary patch
literal 40960
zcmeI5Pi))P8Nf+V){~PkiB<=M=N_J$1(pywo?~P{ll6~cvyL}ajum;EyAA{`(GFpX
zR7EPOcj=U*Eef>gxrd@X73i=_55+D42CUeCtcPvbZ5X;8mcuR^2INqnhwOWgB1QeR
z(lyH}d@m9IeD8bT``+(8hUE8<$159o(=+IfQ?KeCT@sRlC<@O~DhR?P{HEX+q{DC!
z4^Ch&Mvey^P6}s!_`M0HOuk?`$Nk+Pb(A9kB!C2v01`j~51)WMAwwk+`L#g)N&W^q
zOh^C;AOR$R1dzbjg23aFbRlKwuBVlqYEAddZOhQE8Fklm?4`vcF=-|hoSi)`N+(m*
zs$SDvPxl(`{Nf>KqeSiq<PP~ax%;(fFgl3@kN^@u0!RP}1O#TJ3*vyM0O_zaBetFa
zVn?NulaVI?_WmD>Ul1lfg$*VofCL^P0rBp}#);X~S$Q>)gmkMU%W^U~B_yU2S3R%h
z!aUkPHsoO%=NA@i<GOnWG=H@r%U}zRegj8x+bT$Y%Ovu^dYBc^`jBhM;LRj>1A_OX
zf}3*ko6+;OyYiIG-3^@Chc}aL)7Cwweg+N~EN9oWhiV85=imC-Uq1PS!XzVUb;uL2
zA6kf;6@*dsVqlNWak9kbHFcp3f4uPAGhXH=_wV0-g$r97Ht!kyn2l&U6*&oPGo6}n
zo#y~Vp{Bd;b*Elof-h^E)z5sk8<xfFuypHeg7wES=vcqQUJHhg{Nf1xL5H$s9b$g;
z-rMg##14_55dpNXszW%L)+Vt1Rdq-|+ib#oSC|*-f&qg`hmQAPe-(~=nF|s}umu<L
zSz-f%4K%%O=v>feGib0yRl5how;+?kPrUiLd;a6%Gq<;HOvc|mmHyXv-+1NEug6nQ
zOZFe{9ewBL?EODR9Dz9GmvD#)2_OL^fCP{L5_n(&FV3b`*`2t=_E*`{;|nrBf@gRB
zFuY3G6En*TB=D^M;E%`2OJL{NNOp!l)+N~czew&0@D~#jKmter2_OL^FvbK9iPE&l
zgJo{gfB(NNkk82NF>W$84GACtB!C2vz}OI&l@16bBoEZ!@Be%fVr-~jLy!OxKmter
z2_S)k2(b7+-v1Axg!xDS2_OL^fCR>i0N(${t3R<dNB{{S0VIF~Mj^oF|MC5Q6jG>z
z1dsp{Kmtf$d<fwEe|-89TY?0T01`j~NMKX~GWlGXkYN44UGn?*3^^~|ks7g2WAnm0
z;@^dnkjawt!Kmg?4+$WFM?v87L_#@zTHNw<SpU;~$%3zXXs+>6!?4TUr-|jFnk%Wa
zl)IQ$X_s!=)~iMqN@iYrTq4T!wD{&@+@}8@hWu^3!-6lPX{T<&x;|Q?ZqXm)ip!UB
zMOs)d(ZXgvpQU=)g9S;WnOWL!jXJANGc2QO*j`X9l5<VB8b(_ltew+aBFyniT+FXu
z%u=_pT{XR4W!K!b!5LU{#i|CzBfeI2fLDfG&#2I~LP=d!qfObi^zxo%x{#}F?`y0T
zeYD*2RcKV7jL=hWzZT|asb^LVml+GI%<|8&XyA=b{q|H$w`S@48iN-2ouy{E8|;oz
zG3u}sY`5IqGi%yUjk=?iEwj8AIG3fnh8-5DS&m+5EfCw*s5!37mzE7Xt)`oGx8;}_
zI!!MZ*PhQ6x9IoPEt=abt*;eeusp98N?FRe@<r;R%?!cQ74^B?X1+wfouyUhnrZK9
z+m=(_1I+M!ng9Xon-F2ushB%v8(Cjln}xNFO*J|`BLl28^db^IfU-2KBh-kFnoQ<&
zJfS=@EefVxF<x$6M0&$>__^j^6WXG`pV$$Ja^i&8T;P{d)$pJLJw4WO88JzE&)BE=
z^}=fR1w>noh|tYaf2c>)Gjp+oa^b`vr&tZzBHOa1me|rmLNNbNe)lN;(ZV{B01`j~
zNB{{Sf&ZGocOKkA5dSBCdhoJX0tp}iB!C2v0226rA}~L)GjhBr91&j?$S+7NQJI*I
z{~`W@REf_?Q{oq~zd{D4hflyu5oLZ>Y|io65yZ2sie}n74s0N{)IOhT#f#PoE9#GE
zTkh-8^+H5O)9jnh8jD%Mm!9ed1QWU4P>^Zr$A~gJE8diOP^xXNee{iv099MwZ9E7Q
zhT*rK+6HPwgP@V(e)LsiLD4=%gNWWiP0TDEPbjOigW_TB25F1!<Hrg|6Ur$?^douf
zLs_oohqn1~s-qL4*{lQjc@PP&I91)WGtFm@5JgeM8=4=SXBN5M(GI*^6h<livO$nH
zLMDvAc7?_vHe5AbSGV9hU=4_+x8l|i*sSrm>rj-P9(M;rGntH>P@Yl-ar7Z9^4&4G
zTzF*BJ4}>?b7C{)4`Qcoz);j6zUhxv({_zAj7t`0uR!eI)GhZ&2U_Ud&LWI&zhX39
zgXPn0Pcti-)k6v8$L9t)+OM>>*iU-+#-&N3oI5ApTH>A7Pp6;kP)C>c(};K*^bswp
z&#6VVu&iF8t;rN<>gMGYHcPXdyRw{HQCZ_x)VvyzK)75j7WpL3T4AlUmdoe2SPUJ6
z>IzH?_=HVwU;9!QndxfPM!y-5Xq-N{62l(osW+2p&LoMlv?Mlv;*bA*yR1Rr-mo+l
zie}RrZm(#v17sfsnjUS^p|hr*3o6r`BSd-fNwI0Ry<_9iGhsTW!@(YDve(BR9h!FG
zvlsfV!G?a=;gK+Y_asSBWv{2jL_(RH7F~WpwI&$+ISK!SbX-0i3T*VWMpn9G8Y6uF
z|Lo!4=CB4NfCP{L5<mh-;D1EmiQ)AG^Z&!OFc%3R0VIF~kN^@GCj!Cy|2VY?+kym;
f01`j~NB{{8CxG|=;e0R`2_OL^fCP}hI1=~|nNt8i

literal 0
HcmV?d00001

diff --git a/UnitTests/DataImport/DataImportResources/TestChromeData/v32 Excluded/Login Data for Account b/UnitTests/DataImport/DataImportResources/TestChromeData/v32 Excluded/Login Data for Account
new file mode 100644
index 0000000000000000000000000000000000000000..3cd1c61467128db9a6e4d022234bd4522b69b7e6
GIT binary patch
literal 40960
zcmeI54R93I9l-bFa+`#Nd=R*NK;1w!IdUPnd;keXyd*~wxm=P<_^w^{?lxh~-UqjP
ziGg9@_)wHUnW2u=)+s75l;VIeQlKzcTdbghLY>w^g-VN46&$Ic!btme_wI6+TtFN%
z!tme8?Z<on_xAt)z4x-a|NrdU))osdadf-K=VB$g6pP1j9D9(aF$^QnF9ZET%P_Q~
z2_2v<9<e{zE`d#$^-!EFOkI;@Qq|31;*bsmfB+Bx0zd!=3_XG5xMXw^l6noJE>dUF
z1{NRy1b_e#00KbZjvz3aB&VbatSB)KkITzSyj|d!F3u<N9(SoRi6C>*LPsU3I5{rO
z<zl^zD6x`XEHow}8%gR2MjfF(r~Z9M7z|1R0U!VbfB+Bx0wDr9<P?06y8v<+nS%%K
z0f^z`xP*v10Qvc!(3as;rnW3p6>Nb35C8%|00_h)5SUM7wX}@PO({vP(<a2nC$uFe
zC&$NUVA>4r0!i|Ul|@Bvu3MbYgPut3yqlFgz6ofzNbq#<?wd=n4hMQR<$P||#g&%O
zUB=?|m+$M$)94KI?Bg~2SO0#)4eP*)D%URl;&%&Zq&pQue>rX~$FbdhK~S=h{E~T3
zEeh{HwfZ6!;BeW-f{=~zLu#W#@c5nI$xzq4moCys^KWaYU_Nm!WT<Q2jxzcApP(9X
zs*&2Pq{9jb00AHX1b_e#xHSSZDfzCb4!_~;iE<Ov?-u8V1qf^|s<$AeF5!k^G3r%s
zD{fdyjsL%jQD0Dhqh7r=b3qyi00AHX1b_e#_+bc)BFWMOvB&LTTplMUFfLAFovg%W
z6J!Ohq;qbGmv~MLXAy}wnT0F23nHOD{~yGtGt|K!#sWAi5C8%|00;m9AaF|ra><+c
z8VH)aiPu1l|64HXKCR`J&JDsq00;m9AOHk{1Ol@N4Yr~b#p^E^8s13UoK(BlUAE->
z6z=}4!e{sV<9fE{?>n$rzx{flt?mWe192<1Z<_nns*S5h@7-IA9pA;Rocj3NGgB_n
zC2LOZ-nsblk+bja`Xsw}o#Tb#j`#K$X06@danYan^4@va`0Ss(^=3)M#MKLS9r$`%
zO48D?TgP5GEfMz1nfk5!TD#YOcE!{z?90S@H$Q9tOn>IB*PqKvI`#46&mYjaQYJm7
zIeg_mA8b1Mj^SA5=O?bOeC6XOethMysq*Byx@E$Jd(AtyY-OKrU%ctlpVVLYYx?2E
zSv6Nb49voVmc_;8Y<OkC)jyuYdU_A8c>bpwPW}1ZwDzOhMx_@{DPQ-xn6&>-2Y>Wp
z@}6<;J$>m{dt5%tyOm!$mwxg_wf+4kMtrz+L#4FoVurKsnQ`kcKXm$^n>O2eKOH}P
z{D~=xr|()da${H9FMf0Gz?vmX*PPwc@_u&ClH=cOcfN>!F!9{^b(*QaG>uq0W?y>!
zQK{!r^IxvT?XBXH#yqeYH!ZAW=Fhq3Bze8QId^^G+ShikJ#;AW+=arA7Hy;t9oT*9
z=LL=1T8`~_(z@G!Y}woX-AzxPUi04qV%Z}*Ka#Kixb_bi`hx`s00AHX1c1PuLty#+
z=+eA$N#Yhf{^j#T`mgcyUOb*iw<c;632%2QmRg#fo`7Ao>c2b{hvkNL#%3h6&L2@O
zr={Xqsa4YxOfR+0FUd?C6;Gr+JSL$+&dEgb!6kjNT%X-JSbc++q_O&MdRJWhUy)bc
zq+{8$%WJ&VMn^%7Fl(l{+s5(Jipn2#71W!{%Dvq+U1h$xVpsFR;-=ZvjeK))wP<Re
zKC{tVVXAda6vP7F)yYrx@%Gy8PS@O?M@ps{%Ub=#w$|>dDjFZ3=_+$H6wUOuwp$vE
zT`k>{dW2~e_Li!ul99$@qtRGVQ8uyMSTeDEa%stAWBDZc`j004fdvQv0U!VbfB+D<
ziwMZ^f4KhNMGX$70s$ZZ1b_e#Km_3W58VI=00AHX1c1QZM<AK{3X3Dr{C^$PaZL_Y
zNgg5n#G6DR_6+_nY#hpz7jnzp2L>z#0(TOD<~Xgcpa7pEv1tBJaiM^|>cNQILO<tr
z^gq<r*vzIjGu>vYwwURDvib6?D+W|j|JZ1f(q(1gy(5%5)fa};wI-?tHI2^q_;@tG
z3*+|*^h}ejrru<utxavTb%w=apjn56CM1o_G|-_*UKmc`T<ATeP_c-dE>`e!ed%6S
z6uUh>=fD!-9JNHXrK#FLi+;O{mj()pe1{t;<KtMt6{<W!SM&lE$2d@~#5w5(Yn!>w
z9BGPOU>%(TFQQz=-owaN#E=pl3y@JUGQyflJ>GD>ftGj|dPxga7Cxmx`78?~zs^Og
z_BeUbE3iF`+(se24K(lQ54N3iay~Q_Z2xqzllL-<IG=}c2)v^+q@027;N0N?UctjU
zgA>H|Nq9Y?s7x&zZZt2;`^2DPd{}9^+1Ai#vdy8Vo9ED`8Es7sR@7M<&DJ&pt#DN)
zsf#o-44$qvPczN1w9&-|+U4ou-5rcw@HjdVX7qiU5CXYv!U)kzZ+tu7hb(4YGpr3Q
zGt80wGoph9yIw?!(t!+g_#9!0$X=t@7ihG)saZJ2yPe#l!9m3OC698<sAGaLs@u4B
zoYIXPi3f_5p(MYgC;y13VI-&IcXB<nrO8^?e*n=zBO>S;Xtk?Hi0ktRt!~Q5!IYBE
zz!>E<iT<@IQGba4-%0=EL02FE1b_e#00Q3+0r3BSKOCVJAOHk_01yBIcM^eQjSWk}
zS7Fq4iqJaavNZ2#=8{fL9+`n(BhH`<ScaZ}lt$?abMZi);yXhAEWyd}?sgAaA>UHp
zaYoQz)L^YO&!+p*RXMuJ8j(il%inZnWS<rK(vy!v&P1i&aDqO-rck=vTzo~c;*{#E
zu5T|r%0bna-oNsYn=tHt8*sKkB_f@m5ye&SD<eBaV-yW}^loONzBE;<tIHkCAJ%6O
zW9-`>VI8j3-K)b@PoDgtERj)N+sb}gR3k)Y%MG9$hdkj<kBjBq`oM!pluoC^mout!
zUanD*8y<yMNeX)@)v_TcZv>gJ|FxfM9QlS_yeP5)`VN>M`O<@aYvkDUDt^}+qKwwK
zzcW;?*C%UrRl31Ast}CICIt-_#j_|4qjW{(cpy#f#2z1qx*~)8n`&?6-6H2e{ZjU`
zJLNgbSV2sRLW>$Vx`@m-Rxvu?%lc3i4DZy}C2Do^$_G<4R%yl<D|OiN`UFZ>UXHIU
zRT?dpOsurTs3wgi5ut6UjcA*Bn%QQy)|gxAU@!%lI-|K(j?&baT5C+TX4&{!v&9^d
zg52e1n@tJgG*}zj8cY_;9NCAC63w+JET9tRr5@%x4Kf~08Rw4`0g3d}hel$!1qPf~
zuMgzJQ@YYpJn*pE{|C-i7h?jugeZ_P1nB&47j%f87jM9T(g*S=N>@>V2lzhi2F#fk
zdnUd3fjSN-N#~2`^9O7PCwKjDgGadW`v*y6_q-DF=^p~n+q7C;UKTDY11cC`P-7D6
z0TnfTq6FmL6YN>}QDF>t{vZ0wJDdRs00AHX1c1QpMgZdfw_A&VK0p8n00AHX1csgf
pJpT_pV>km400KY&2mpcGjR0K#Z?_f!eSiQE00KY&2n;=e{{!tc{`UX?

literal 0
HcmV?d00001

diff --git a/UnitTests/DataImport/DataImportResources/TestChromeData/v32/Login Data b/UnitTests/DataImport/DataImportResources/TestChromeData/v32/Login Data
index 76ab8de045152236833e62ee84aebff573ed9e65..6ffc00b624f06022593919113f0210425583e72e 100644
GIT binary patch
delta 281
zcmZoTz|?SnX@WE(=R_H2R!#=JaIcLi3&gpYnL`*@3z>VELpC;YGS^oyFff$3^U8_>
z=^P-=aOdUaHD=)D;N$@Ez#tSP#ET-tz`()4kx^pCDG8PXN+YR<N|qE9Tj}eUmzV1m
zrKY50mgptt=j!L=r)TErHz#pO1qO;L3L0&mCY{H^#muhC!0yeym|b;aBNw}|8PHlI
zSy5g_Ag!g!4z_|ZqXdtEoSVJXw{Y-tashQPvu|f$-_CxIea~h=j+N}}EX=%&oQlko
N1J&g>-!)gx0022tIGX?f

delta 90
zcmV-g0Hyzczyg540+1U42$38^1qc8xS4pvCpce=N1B?I#aRZF8fq(<Ej2n0a2m%L7
w00&D4qp^WM2eV5jv<M9d3IG5A0tdVR2i^y~vk?fe2Ld4jlTjxhv)wl%XjuLjTL1t6


From 7d524cce0a6a5b8f7999cee64eef63a9b03ec799 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= <dus7@users.noreply.github.com>
Date: Fri, 15 Mar 2024 14:03:57 +0100
Subject: [PATCH 07/16] Roll back CPM post-rollout cleanup (#2430)

Task/Issue URL:
https://app.asana.com/0/414235014887631/1206850469068241/f
Tech Design URL:
CC:

**Description**:

Updates the BSK reference as a part of rolling back
https://github.com/duckduckgo/macos-browser/pull/2316

**Steps to test this PR**:
1. CI is green

<!--
Tagging instructions
If this PR isn't ready to be merged for whatever reason it should be
marked with the `DO NOT MERGE` label (particularly if it's a draft)
If it's pending Product Review/PFR, please add the `Pending Product
Review` label.

If at any point it isn't actively being worked on/ready for
review/otherwise moving forward (besides the above PR/PFR exception)
strongly consider closing it (or not opening it in the first place). If
you decide not to close it, make sure it's labelled to make it clear the
PRs state and comment with more information.
-->

---
###### 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)
---
 DuckDuckGo.xcodeproj/project.pbxproj                        | 2 +-
 .../xcshareddata/swiftpm/Package.resolved                   | 6 +++---
 LocalPackages/DataBrokerProtection/Package.swift            | 2 +-
 LocalPackages/NetworkProtectionMac/Package.swift            | 2 +-
 LocalPackages/SubscriptionUI/Package.swift                  | 2 +-
 5 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj
index 76a2d243ff..23d4cdecf1 100644
--- a/DuckDuckGo.xcodeproj/project.pbxproj
+++ b/DuckDuckGo.xcodeproj/project.pbxproj
@@ -13749,7 +13749,7 @@
 			repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit";
 			requirement = {
 				kind = exactVersion;
-				version = 124.0.0;
+				version = 124.1.0;
 			};
 		};
 		AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index e8258208b3..5d9e06d99f 100644
--- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -32,8 +32,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/duckduckgo/BrowserServicesKit",
       "state" : {
-        "branch" : "bartek/stub-objects",
-        "revision" : "968c429c464688c641d100eab584aa95b039e371"
+        "revision" : "bcafd206465427c560f9f581def57d8eef53748c",
+        "version" : "124.1.0"
       }
     },
     {
@@ -165,7 +165,7 @@
     {
       "identity" : "trackerradarkit",
       "kind" : "remoteSourceControl",
-      "location" : "https://github.com/duckduckgo/TrackerRadarKit.git",
+      "location" : "https://github.com/duckduckgo/TrackerRadarKit",
       "state" : {
         "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff",
         "version" : "1.2.2"
diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift
index dd701eff21..e07c2c873c 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: "124.0.0"),
+        .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.1.0"),
         .package(path: "../PixelKit"),
         .package(path: "../SwiftUIExtensions"),
         .package(path: "../XPCHelper"),
diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift
index 000dc4e3f9..5c07540155 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: "124.0.0"),
+        .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.1.0"),
         .package(path: "../XPCHelper"),
         .package(path: "../SwiftUIExtensions"),
         .package(path: "../LoginItems"),
diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift
index 98ceb2ad67..c29571c2ce 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: "124.0.0"),
+        .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "124.1.0"),
         .package(path: "../SwiftUIExtensions")
     ],
     targets: [

From c299d42a0f292e156efeb392d89195c41de93e13 Mon Sep 17 00:00:00 2001
From: Juan Manuel Pereira <juanmanuel.pereira1@gmail.com>
Date: Fri, 15 Mar 2024 10:36:10 -0300
Subject: [PATCH 08/16] DBP: Debug scan model implementation (#2421)

---
 .../CCF/DataBrokerProtectionFeature.swift     |   4 +-
 .../CCF/WebViewHandler.swift                  |  71 +++
 .../DebugUI/DataBrokerRunCustomJSONView.swift |  60 ++-
 .../DataBrokerRunCustomJSONViewModel.swift    | 425 ++++++++++++++++--
 .../DebugUI/DebugScanOperation.swift          | 181 ++++++++
 .../Model/DataBrokerProtectionProfile.swift   |   2 +-
 .../Model/ProfileQuery.swift                  |  15 +
 .../Operations/OptOutOperation.swift          |   2 +-
 .../Operations/ScanOperation.swift            |   2 +-
 .../DataBrokerProtectionFeatureTests.swift    |   2 +-
 .../DataBrokerProtectionTests/Mocks.swift     |   8 +
 11 files changed, 700 insertions(+), 72 deletions(-)
 create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift

diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionFeature.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionFeature.swift
index c596488888..be97096c4e 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionFeature.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionFeature.swift
@@ -24,7 +24,7 @@ import Common
 
 protocol CCFCommunicationDelegate: AnyObject {
     func loadURL(url: URL) async
-    func extractedProfiles(profiles: [ExtractedProfile]) async
+    func extractedProfiles(profiles: [ExtractedProfile], meta: [String: Any]?) async
     func captchaInformation(captchaInfo: GetCaptchaInfoResponse) async
     func solveCaptcha(with response: SolveCaptchaResponse) async
     func success(actionId: String, actionType: ActionType) async
@@ -101,7 +101,7 @@ struct DataBrokerProtectionFeature: Subfeature {
                 await delegate?.onError(error: DataBrokerProtectionError.malformedURL)
             }
         case .extract(let profiles):
-            await delegate?.extractedProfiles(profiles: profiles)
+            await delegate?.extractedProfiles(profiles: profiles, meta: success.meta)
         case .getCaptchaInfo(let captchaInfo):
             await delegate?.captchaInformation(captchaInfo: captchaInfo)
         case .solveCaptcha(let response):
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift
index c89644eec4..ab98fa98b9 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift
@@ -25,6 +25,8 @@ import Common
 protocol WebViewHandler: NSObject {
     func initializeWebView(showWebView: Bool) async
     func load(url: URL) async throws
+    func takeSnaphost(path: String, fileName: String) async throws
+    func saveHTML(path: String, fileName: String) async throws
     func waitForWebViewLoad(timeoutInSeconds: Int) async throws
     func finish() async
     func execute(action: Action, data: CCFRequestData) async
@@ -122,6 +124,75 @@ final class DataBrokerProtectionWebViewHandler: NSObject, WebViewHandler {
     func evaluateJavaScript(_ javaScript: String) async throws {
         _ = webView?.evaluateJavaScript(javaScript, in: nil, in: WKContentWorld.page)
     }
+
+    func takeSnaphost(path: String, fileName: String) async throws {
+        let script = "document.body.scrollHeight"
+
+        let result = try await webView?.evaluateJavaScript(script)
+
+        if let height = result as? CGFloat {
+            webView?.frame = CGRect(origin: .zero, size: CGSize(width: 1024, height: height))
+            let configuration = WKSnapshotConfiguration()
+            configuration.rect = CGRect(x: 0, y: 0, width: webView?.frame.size.width ?? 0.0, height: height)
+            if let image = try await webView?.takeSnapshot(configuration: configuration) {
+                saveToDisk(image: image, path: path, fileName: fileName)
+            }
+        }
+    }
+
+    func saveHTML(path: String, fileName: String) async throws {
+        let result = try await webView?.evaluateJavaScript("document.documentElement.outerHTML")
+        let fileManager = FileManager.default
+
+        if let htmlString = result as? String {
+            do {
+                if !fileManager.fileExists(atPath: path) {
+                    try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
+                }
+
+                let fileURL = URL(fileURLWithPath: "\(path)/\(fileName)")
+                try htmlString.write(to: fileURL, atomically: true, encoding: .utf8)
+                print("HTML content saved to file: \(fileURL)")
+            } catch {
+                print("Error writing HTML content to file: \(error)")
+            }
+        }
+    }
+
+    private func saveToDisk(image: NSImage, path: String, fileName: String) {
+        guard let tiffData = image.tiffRepresentation else {
+            // Handle the case where tiff representation is not available
+            return
+        }
+
+        // Create a bitmap representation from the tiff data
+        guard let bitmapImageRep = NSBitmapImageRep(data: tiffData) else {
+            // Handle the case where bitmap representation cannot be created
+            return
+        }
+
+        let fileManager = FileManager.default
+
+        if !fileManager.fileExists(atPath: path) {
+            do {
+                try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
+            } catch {
+                print("Error creating folder: \(error)")
+            }
+        }
+
+        if let pngData = bitmapImageRep.representation(using: .png, properties: [:]) {
+            // Save the PNG data to a file
+            do {
+                let fileURL = URL(fileURLWithPath: "\(path)/\(fileName)")
+                try pngData.write(to: fileURL)
+            } catch {
+                print("Error writing PNG: \(error)")
+            }
+        } else {
+            print("Error png data was not respresented")
+        }
+    }
 }
 
 extension DataBrokerProtectionWebViewHandler: WKNavigationDelegate {
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift
index 33671421d5..22b8c6b90a 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONView.swift
@@ -28,26 +28,42 @@ struct DataBrokerRunCustomJSONView: View {
         if viewModel.results.isEmpty {
             VStack(alignment: .leading) {
                 Text("macOS App version: \(viewModel.appVersion())")
-                Text("C-S-S version: \(viewModel.contentScopeScriptsVersion())")
 
                 Divider()
 
-                HStack {
-                    TextField("First name", text: $viewModel.firstName)
-                        .padding()
-                    TextField("Last name", text: $viewModel.lastName)
-                        .padding()
-                    TextField("Middle", text: $viewModel.middle)
-                        .padding()
+                ForEach(viewModel.names.indices, id: \.self) { index in
+                    HStack {
+                        TextField("First name", text: $viewModel.names[index].first)
+                            .padding()
+                        TextField("Middle", text: $viewModel.names[index].middle)
+                            .padding()
+                        TextField("Last name", text: $viewModel.names[index].last)
+                            .padding()
+                    }
+                }
+
+                Button("Add other name") {
+                    viewModel.names.append(.empty())
                 }
 
                 Divider()
 
-                HStack {
-                    TextField("City", text: $viewModel.city)
-                        .padding()
-                    TextField("State", text: $viewModel.state)
-                        .padding()
+                ForEach(viewModel.addresses.indices, id: \.self) { index in
+                    HStack {
+                        TextField("City", text: $viewModel.addresses[index].city)
+                            .padding()
+                        TextField("State (two characters format)", text: $viewModel.addresses[index].state)
+                            .onChange(of: viewModel.addresses[index].state) { newValue in
+                                if newValue.count > 2 {
+                                    viewModel.addresses[index].state = String(newValue.prefix(2))
+                                }
+                            }
+                            .padding()
+                    }
+                }
+
+                Button("Add other address") {
+                    viewModel.addresses.append(.empty())
                 }
 
                 Divider()
@@ -76,6 +92,14 @@ struct DataBrokerRunCustomJSONView: View {
                 Button("Run") {
                     viewModel.runJSON(jsonString: jsonText)
                 }
+
+                if viewModel.isRunningOnAllBrokers {
+                    ProgressView("Scanning...")
+                } else {
+                    Button("Run all brokers") {
+                        viewModel.runAllBrokers()
+                    }
+                }
             }
             .padding()
             .frame(minWidth: 600, minHeight: 800)
@@ -88,19 +112,19 @@ struct DataBrokerRunCustomJSONView: View {
         } else {
             VStack {
                 VStack {
-                    List(viewModel.results, id: \.name) { extractedProfile in
+                    List(viewModel.results, id: \.id) { scanResult in
                         HStack {
-                            Text(extractedProfile.name ?? "No name")
+                            Text(scanResult.extractedProfile.name ?? "No name")
                                 .padding(.horizontal, 10)
                             Divider()
-                            Text(extractedProfile.addresses?.first?.fullAddress ?? "No address")
+                            Text(scanResult.extractedProfile.addresses?.first?.fullAddress ?? "No address")
                                 .padding(.horizontal, 10)
                             Divider()
-                            Text(extractedProfile.relatives?.joined(separator: ",") ?? "No relatives")
+                            Text(scanResult.extractedProfile.relatives?.joined(separator: ",") ?? "No relatives")
                                 .padding(.horizontal, 10)
                             Divider()
                             Button("Opt-out") {
-                                viewModel.runOptOut(extractedProfile: extractedProfile)
+                                viewModel.runOptOut(scanResult: scanResult)
                             }
                         }
                     }.navigationTitle("Results")
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift
index e0241d5542..0cb75a8f1d 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift
@@ -22,6 +22,49 @@ import Common
 import ContentScopeScripts
 import Combine
 
+struct ExtractedAddress: Codable {
+    let state: String
+    let city: String
+}
+
+struct UserData: Codable {
+    let firstName: String
+    let lastName: String
+    let middleName: String?
+    let state: String
+    let email: String?
+    let city: String
+    let age: Int
+    let addresses: [ExtractedAddress]
+}
+
+struct ProfileUrl: Codable {
+    let profileUrl: String
+    let identifier: String
+}
+
+struct ScrapedData: Codable {
+    let name: String?
+    let alternativeNamesList: [String]?
+    let age: String?
+    let addressCityState: String?
+    let addressCityStateList: [ExtractedAddress]?
+    let relativesList: [String]?
+    let profileUrl: ProfileUrl?
+}
+
+struct ExtractResult: Codable {
+    let scrapedData: ScrapedData
+    let result: Bool
+    let score: Int
+    let matchedFields: [String]
+}
+
+struct Metadata: Codable {
+    let userData: UserData
+    let extractResults: [ExtractResult]
+}
+
 struct AlertUI {
     var title: String = ""
     var description: String = ""
@@ -30,28 +73,80 @@ struct AlertUI {
         AlertUI(title: "No results", description: "No results were found.")
     }
 
+    static func finishedScanningAllBrokers() -> AlertUI {
+        AlertUI(title: "Finished!", description: "We finished scanning all brokers. You should find the data inside ~/Desktop/PIR-Debug/")
+    }
+
     static func from(error: DataBrokerProtectionError) -> AlertUI {
         AlertUI(title: error.title, description: error.description)
     }
 }
 
-final class DataBrokerRunCustomJSONViewModel: ObservableObject {
+final class NameUI: ObservableObject {
+    let id = UUID()
+    @Published var first: String
+    @Published var middle: String
+    @Published var last: String
+
+    init(first: String, middle: String = "", last: String) {
+        self.first = first
+        self.middle = middle
+        self.last = last
+    }
 
-    @Published var firstName: String = ""
-    @Published var lastName: String = ""
-    @Published var middle: String = ""
-    @Published var city: String = ""
-    @Published var state: String = ""
+    static func empty() -> NameUI {
+        .init(first: "", middle: "", last: "")
+    }
+
+    func toModel() -> DataBrokerProtectionProfile.Name {
+        .init(firstName: first, lastName: last, middleName: middle.isEmpty ? nil : middle)
+    }
+}
+
+final class AddressUI: ObservableObject {
+    let id = UUID()
+    @Published var city: String
+    @Published var state: String
+
+    init(city: String, state: String) {
+        self.city = city
+        self.state = state
+    }
+
+    static func empty() -> AddressUI {
+        .init(city: "", state: "")
+    }
+
+    func toModel() -> DataBrokerProtectionProfile.Address {
+        .init(city: city, state: state)
+    }
+}
+
+struct ScanResult {
+    let id = UUID()
+    let dataBroker: DataBroker
+    let profileQuery: ProfileQuery
+    let extractedProfile: ExtractedProfile
+}
+
+final class DataBrokerRunCustomJSONViewModel: ObservableObject {
     @Published var birthYear: String = ""
-    @Published var results = [ExtractedProfile]()
+    @Published var results = [ScanResult]()
     @Published var showAlert = false
     @Published var showNoResults = false
+    @Published var isRunningOnAllBrokers = false
+    @Published var names = [NameUI.empty()]
+    @Published var addresses = [AddressUI.empty()]
+
     var alert: AlertUI?
     var selectedDataBroker: DataBroker?
 
     let brokers: [DataBroker]
 
     private let runnerProvider: OperationRunnerProvider
+    private let privacyConfigManager: PrivacyConfigurationManaging
+    private let contentScopeProperties: ContentScopeProperties
+    private let csvColumns = ["name_input", "age_input", "city_input", "state_input", "name_scraped", "age_scraped", "address_scraped", "relatives_scraped", "url", "broker name", "screenshot_id", "error", "matched_fields", "result_match", "expected_match"]
 
     init() {
         let privacyConfigurationManager = PrivacyConfigurationManagingMock()
@@ -69,45 +164,202 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject {
         let contentScopeProperties = ContentScopeProperties(gpcEnabled: false,
                                                             sessionKey: sessionKey,
                                                             featureToggles: features)
+
         self.runnerProvider = DataBrokerOperationRunnerProvider(
             privacyConfigManager: privacyConfigurationManager,
             contentScopeProperties: contentScopeProperties,
             emailService: EmailService(),
             captchaService: CaptchaService())
+        self.privacyConfigManager = privacyConfigurationManager
+        self.contentScopeProperties = contentScopeProperties
 
         let fileResources = FileResources()
         self.brokers = fileResources.fetchBrokerFromResourceFiles() ?? [DataBroker]()
     }
 
-    func runJSON(jsonString: String) {
-        if firstName.isEmpty || lastName.isEmpty || city.isEmpty || state.isEmpty || birthYear.isEmpty {
+    func runAllBrokers() {
+        isRunningOnAllBrokers = true
+
+        let brokerProfileQueryData = createBrokerProfileQueryData()
+
+        Task.detached {
+            var scanResults = [DebugScanReturnValue]()
+            let semaphore = DispatchSemaphore(value: 10)
+            try await withThrowingTaskGroup(of: DebugScanReturnValue.self) { group in
+                for queryData in brokerProfileQueryData {
+                    semaphore.wait()
+                    let debugScanOperation = DebugScanOperation(privacyConfig: self.privacyConfigManager, prefs: self.contentScopeProperties, query: queryData) {
+                        true
+                    }
+
+                    group.addTask {
+                        defer {
+                            semaphore.signal()
+                        }
+                        do {
+                            return try await debugScanOperation.run(inputValue: (), stageCalculator: FakeStageDurationCalculator(), showWebView: false)
+                        } catch {
+                            return DebugScanReturnValue(brokerURL: "ERROR - with broker: \(queryData.dataBroker.name)", extractedProfiles: [ExtractedProfile](), brokerProfileQueryData: queryData)
+                        }
+                    }
+                }
+
+                for try await result in group {
+                    scanResults.append(result)
+                }
+
+                self.formCSV(with: scanResults)
+
+                self.finishLoading()
+            }
+        }
+    }
+
+    private func finishLoading() {
+        DispatchQueue.main.async {
+            self.alert = AlertUI.finishedScanningAllBrokers()
             self.showAlert = true
-            self.alert = AlertUI(title: "Error", description: "Some required fields were not entered.")
-            return
+            self.isRunningOnAllBrokers = false
+        }
+    }
+
+    private func formCSV(with scanResults: [DebugScanReturnValue]) {
+        var csvText = csvColumns.map { $0 }.joined(separator: ",")
+        csvText.append("\n")
+
+        for result in scanResults {
+            if let error = result.error {
+                csvText.append(append(error: error, for: result))
+            } else {
+                csvText.append(append(result))
+            }
+        }
+
+        save(csv: csvText)
+    }
+
+    private func append(error: Error, for result: DebugScanReturnValue) -> String {
+        if let dbpError = error as? DataBrokerProtectionError {
+            if dbpError.is404 {
+                return createRowFor(matched: false, result: result, error: "404 - No results")
+            } else {
+                return createRowFor(matched: false, result: result, error: "\(dbpError.title)-\(dbpError.description)")
+            }
+        } else {
+            return createRowFor(matched: false, result: result, error: error.localizedDescription)
+        }
+    }
+
+    private func append(_ result: DebugScanReturnValue) -> String {
+        var resultsText = ""
+
+        if let meta = result.meta{
+            do {
+                let jsonData = try JSONSerialization.data(withJSONObject: meta, options: [])
+                let decoder = JSONDecoder()
+                let decodedMeta = try decoder.decode(Metadata.self, from: jsonData)
+
+                for extractedResult in decodedMeta.extractResults {
+                    resultsText.append(createRowFor(matched: extractedResult.result, result: result, extractedResult: extractedResult))
+                }
+            } catch {
+                print("Error decoding JSON: \(error)")
+            }
+        } else {
+            print("No meta object")
         }
 
+        return resultsText
+    }
+
+    private func createRowFor(matched: Bool,
+                              result: DebugScanReturnValue,
+                              error: String? = nil,
+                              extractedResult: ExtractResult? = nil) -> String {
+        let matchedString = matched ? "TRUE" : "FALSE"
+        let profileQuery = result.brokerProfileQueryData.profileQuery
+
+        var csvRow = ""
+
+        csvRow.append("\(profileQuery.fullName),") // Name (input)
+        csvRow.append("\(profileQuery.age),") // Age (input)
+        csvRow.append("\(profileQuery.city),") // City (input)
+        csvRow.append("\(profileQuery.state),") // State (input)
+
+        if let extractedResult = extractedResult {
+            csvRow.append("\(extractedResult.scrapedData.nameCSV),") // Name (scraped)
+            csvRow.append("\(extractedResult.scrapedData.ageCSV),") // Age (scraped)
+            csvRow.append("\(extractedResult.scrapedData.addressesCSV),") // Address (scraped)
+            csvRow.append("\(extractedResult.scrapedData.relativesCSV),") // Relatives (matched)
+        } else {
+            csvRow.append(",") // Name (scraped)
+            csvRow.append(",") // Age (scraped)
+            csvRow.append(",") // Address (scraped)
+            csvRow.append(",") // Relatives (scraped)
+        }
+
+        csvRow.append("\(result.brokerURL),") // Broker URL
+        csvRow.append("\(result.brokerProfileQueryData.dataBroker.name),") // Broker Name
+        csvRow.append("\(profileQuery.id ?? 0)_\(result.brokerProfileQueryData.dataBroker.name),") // Screenshot name
+
+        if let error = error {
+            csvRow.append("\(error),") // Error
+        } else {
+            csvRow.append(",") // Error empty
+        }
+
+        if let extractedResult = extractedResult {
+            csvRow.append("\(extractedResult.matchedFields.joined(separator: "-")),") // matched_fields
+        } else {
+            csvRow.append(",") // matched_fields
+        }
+
+        csvRow.append("\(matchedString),") // result_match
+        csvRow.append(",") // expected_match
+        csvRow.append("\n")
+
+        return csvRow
+    }
+
+    private func save(csv: String) {
+        do {
+            if let desktopPath = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first?.relativePath {
+                let path = desktopPath + "/PIR-Debug"
+                let fileName = "output.csv"
+                let fileURL = URL(fileURLWithPath: "\(path)/\(fileName)")
+                try csv.write(to: fileURL, atomically: true, encoding: .utf8)
+            } else {
+                os_log("Error getting path")
+            }
+        } catch {
+            os_log("Error writing to file: \(error)")
+        }
+    }
+
+    func runJSON(jsonString: String) {
         if let data = jsonString.data(using: .utf8) {
             do {
                 let decoder = JSONDecoder()
                 let dataBroker = try decoder.decode(DataBroker.self, from: data)
                 self.selectedDataBroker = dataBroker
                 let brokerProfileQueryData = createBrokerProfileQueryData(for: dataBroker)
-
                 let runner = runnerProvider.getOperationRunner()
 
-                Task {
-                    do {
-                        let extractedProfiles = try await runner.scan(brokerProfileQueryData, stageCalculator: FakeStageDurationCalculator(), showWebView: true) { true }
-
-                        DispatchQueue.main.async {
-                            if extractedProfiles.isEmpty {
-                                self.showNoResultsAlert()
-                            } else {
-                                self.results = extractedProfiles
+                for query in brokerProfileQueryData {
+                    Task {
+                        do {
+                            let extractedProfiles = try await runner.scan(query, stageCalculator: FakeStageDurationCalculator(), showWebView: true) { true }
+
+                            DispatchQueue.main.async {
+                                for extractedProfile in extractedProfiles {
+                                    self.results.append(ScanResult(dataBroker: query.dataBroker,
+                                                                   profileQuery: query.profileQuery,
+                                                                   extractedProfile: extractedProfile))
+                                }
                             }
+                        } catch {
+                            print("Error when scanning: \(error)")
                         }
-                    } catch {
-                        showAlert(for: error)
                     }
                 }
             } catch {
@@ -116,18 +368,16 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject {
         }
     }
 
-    func runOptOut(extractedProfile: ExtractedProfile) {
+    func runOptOut(scanResult: ScanResult) {
         let runner = runnerProvider.getOperationRunner()
-        guard let dataBroker = self.selectedDataBroker else {
-            print("No broker selected")
-            return
-        }
-
-        let brokerProfileQueryData = createBrokerProfileQueryData(for: dataBroker)
-
+        let brokerProfileQueryData = BrokerProfileQueryData(
+            dataBroker: scanResult.dataBroker,
+            profileQuery: scanResult.profileQuery,
+            scanOperationData: ScanOperationData(brokerId: 1, profileQueryId: 1, historyEvents: [HistoryEvent]())
+        )
         Task {
             do {
-                try await runner.optOut(profileQuery: brokerProfileQueryData, extractedProfile: extractedProfile, stageCalculator: FakeStageDurationCalculator(), showWebView: true) {
+                try await runner.optOut(profileQuery: brokerProfileQueryData, extractedProfile: scanResult.extractedProfile, stageCalculator: FakeStageDurationCalculator(), showWebView: true) {
                     true
                 }
 
@@ -142,10 +392,54 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject {
         }
     }
 
-    private func createBrokerProfileQueryData(for dataBroker: DataBroker) -> BrokerProfileQueryData {
-        let profile = createProfile()
-        let fakeScanOperationData = ScanOperationData(brokerId: 0, profileQueryId: 0, historyEvents: [HistoryEvent]())
-        return BrokerProfileQueryData(dataBroker: dataBroker, profileQuery: profile.profileQueries.first!, scanOperationData: fakeScanOperationData)
+    private func createBrokerProfileQueryData(for broker: DataBroker) -> [BrokerProfileQueryData] {
+        let profile: DataBrokerProtectionProfile =
+            .init(
+                names: names.map { $0.toModel() },
+                addresses: addresses.map { $0.toModel() },
+                phones: [String](),
+                birthYear: Int(birthYear) ?? 1990
+            )
+        let profileQueries = profile.profileQueries
+        var brokerProfileQueryData = [BrokerProfileQueryData]()
+
+        var profileQueryIndex: Int64 = 1
+        for profileQuery in profileQueries {
+            let fakeScanOperationData = ScanOperationData(brokerId: 0, profileQueryId: profileQueryIndex, historyEvents: [HistoryEvent]())
+            brokerProfileQueryData.append(
+                .init(dataBroker: broker, profileQuery: profileQuery.with(id: profileQueryIndex), scanOperationData: fakeScanOperationData)
+            )
+
+            profileQueryIndex += 1
+        }
+
+        return brokerProfileQueryData
+    }
+
+    private func createBrokerProfileQueryData() -> [BrokerProfileQueryData] {
+        let profile: DataBrokerProtectionProfile =
+            .init(
+                names: names.map { $0.toModel() },
+                addresses: addresses.map { $0.toModel() },
+                phones: [String](),
+                birthYear: Int(birthYear) ?? 1990
+            )
+        let profileQueries = profile.profileQueries
+        var brokerProfileQueryData = [BrokerProfileQueryData]()
+
+        var profileQueryIndex: Int64 = 1
+        for profileQuery in profileQueries {
+            let fakeScanOperationData = ScanOperationData(brokerId: 0, profileQueryId: profileQueryIndex, historyEvents: [HistoryEvent]())
+            for broker in brokers {
+                brokerProfileQueryData.append(
+                    .init(dataBroker: broker, profileQuery: profileQuery.with(id: profileQueryIndex), scanOperationData: fakeScanOperationData)
+                )
+            }
+
+            profileQueryIndex += 1
+        }
+
+        return brokerProfileQueryData
     }
 
     private func showNoResultsAlert() {
@@ -166,21 +460,9 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject {
         }
     }
 
-    private func createProfile() -> DataBrokerProtectionProfile {
-        let names = DataBrokerProtectionProfile.Name(firstName: firstName, lastName: lastName, middleName: middle)
-        let addresses = DataBrokerProtectionProfile.Address(city: city, state: state)
-
-        return DataBrokerProtectionProfile(names: [names], addresses: [addresses], phones: [String](), birthYear: Int(birthYear) ?? 1990)
-    }
-
     func appVersion() -> String {
         AppVersion.shared.versionNumber
     }
-
-    func contentScopeScriptsVersion() -> String {
-        // How can I return this?
-        return "4.59.2"
-    }
 }
 
 final class FakeStageDurationCalculator: StageDurationCalculator {
@@ -367,4 +649,51 @@ extension DataBrokerProtectionError {
         default: return name
         }
     }
+
+    var is404: Bool {
+        switch self {
+        case .httpError(let code):
+            return code == 404
+        default: return false
+        }
+    }
+}
+
+extension ScrapedData {
+
+    var nameCSV: String {
+        if let name = self.name {
+            return name.replacingOccurrences(of: ",", with: "-")
+        } else if let alternativeNamesList = self.alternativeNamesList {
+            return alternativeNamesList.joined(separator: "/").replacingOccurrences(of: ",", with: "-")
+        } else {
+            return ""
+        }
+    }
+
+    var ageCSV: String {
+        if let age = self.age {
+            return age
+        } else {
+            return ""
+        }
+    }
+
+    var addressesCSV: String {
+        if let address = self.addressCityState {
+            return address
+        } else if let addressFull = self.addressCityStateList {
+            return addressFull.map { "\($0.city)-\($0.state)" }.joined(separator: "/")
+        } else {
+            return ""
+        }
+    }
+
+    var relativesCSV: String {
+        if let relatives = self.relativesList {
+            return relatives.joined(separator: "-").replacingOccurrences(of: ",", with: "-")
+        } else {
+            return ""
+        }
+    }
 }
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift
new file mode 100644
index 0000000000..94ee1b481c
--- /dev/null
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift
@@ -0,0 +1,181 @@
+//
+//  DebugScanOperation.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 WebKit
+import BrowserServicesKit
+import UserScript
+import Common
+
+struct DebugScanReturnValue {
+    let brokerURL: String
+    let extractedProfiles: [ExtractedProfile]
+    let error: Error?
+    let brokerProfileQueryData: BrokerProfileQueryData
+    let meta: [String: Any]?
+
+    init(brokerURL: String,
+         extractedProfiles: [ExtractedProfile] = [ExtractedProfile](),
+         error: Error? = nil,
+         brokerProfileQueryData: BrokerProfileQueryData,
+         meta: [String: Any]? = nil) {
+        self.brokerURL = brokerURL
+        self.extractedProfiles = extractedProfiles
+        self.error = error
+        self.brokerProfileQueryData = brokerProfileQueryData
+        self.meta = meta
+    }
+}
+
+final class DebugScanOperation: DataBrokerOperation {
+    typealias ReturnValue = DebugScanReturnValue
+    typealias InputValue = Void
+
+    let privacyConfig: PrivacyConfigurationManaging
+    let prefs: ContentScopeProperties
+    let query: BrokerProfileQueryData
+    let emailService: EmailServiceProtocol
+    let captchaService: CaptchaServiceProtocol
+    var webViewHandler: WebViewHandler?
+    var actionsHandler: ActionsHandler?
+    var continuation: CheckedContinuation<DebugScanReturnValue, Error>?
+    var extractedProfile: ExtractedProfile?
+    var stageCalculator: StageDurationCalculator?
+    private let operationAwaitTime: TimeInterval
+    let shouldRunNextStep: () -> Bool
+    var retriesCountOnError: Int = 0
+    var scanURL: String?
+
+    private let fileManager = FileManager.default
+    private let debugScanContentPath: String?
+
+    init(privacyConfig: PrivacyConfigurationManaging,
+         prefs: ContentScopeProperties,
+         query: BrokerProfileQueryData,
+         emailService: EmailServiceProtocol = EmailService(),
+         captchaService: CaptchaServiceProtocol = CaptchaService(),
+         operationAwaitTime: TimeInterval = 3,
+         shouldRunNextStep: @escaping () -> Bool
+    ) {
+        self.privacyConfig = privacyConfig
+        self.prefs = prefs
+        self.query = query
+        self.emailService = emailService
+        self.captchaService = captchaService
+        self.operationAwaitTime = operationAwaitTime
+        self.shouldRunNextStep = shouldRunNextStep
+        if let desktopPath = fileManager.urls(for: .desktopDirectory, in: .userDomainMask).first?.relativePath {
+            self.debugScanContentPath = desktopPath + "/PIR-Debug"
+        } else {
+            self.debugScanContentPath = nil
+        }
+    }
+
+    func run(inputValue: Void,
+             webViewHandler: WebViewHandler? = nil,
+             actionsHandler: ActionsHandler? = nil,
+             stageCalculator: StageDurationCalculator, // We do not need it for scans - for now.
+             showWebView: Bool) async throws -> DebugScanReturnValue {
+        try await withCheckedThrowingContinuation { continuation in
+            self.continuation = continuation
+            Task {
+                await initialize(handler: webViewHandler, isFakeBroker: query.dataBroker.isFakeBroker, showWebView: showWebView)
+
+                do {
+                    let scanStep = try query.dataBroker.scanStep()
+                    if let actionsHandler = actionsHandler {
+                        self.actionsHandler = actionsHandler
+                    } else {
+                        self.actionsHandler = ActionsHandler(step: scanStep)
+                    }
+                    if self.shouldRunNextStep() {
+                        await executeNextStep()
+                    } else {
+                        failed(with: DataBrokerProtectionError.cancelled)
+                    }
+                } catch {
+                    failed(with: DataBrokerProtectionError.unknown(error.localizedDescription))
+                }
+            }
+        }
+    }
+
+    func runNextAction(_ action: Action) async {
+        if action as? ExtractAction != nil {
+            do {
+                if let path = self.debugScanContentPath {
+                    let fileName = "\(query.profileQuery.id ?? 0)_\(query.dataBroker.name)"
+                    try await webViewHandler?.takeSnaphost(path: path + "/screenshots/", fileName: "\(fileName).png")
+                    try await webViewHandler?.saveHTML(path: path + "/html/", fileName: "\(fileName).html")
+                }
+            } catch {
+                print("Error: \(error)")
+            }
+        }
+
+        await webViewHandler?.execute(action: action, data: .userData(query.profileQuery, self.extractedProfile))
+    }
+
+    func extractedProfiles(profiles: [ExtractedProfile], meta: [String: Any]?) async {
+        if let scanURL = self.scanURL {
+            let debugScanReturnValue = DebugScanReturnValue(
+                brokerURL: scanURL,
+                extractedProfiles: profiles,
+                brokerProfileQueryData: query,
+                meta: meta
+            )
+            complete(debugScanReturnValue)
+        }
+
+        await executeNextStep()
+    }
+
+    func completeWith(error: Error) async {
+        if let scanURL = self.scanURL {
+            let debugScanReturnValue = DebugScanReturnValue(brokerURL: scanURL, error: error, brokerProfileQueryData: query)
+            complete(debugScanReturnValue)
+        }
+
+        await executeNextStep()
+    }
+
+    func executeNextStep() async {
+        retriesCountOnError = 0 // We reset the retries on error when it is successful
+        os_log("SCAN Waiting %{public}f seconds...", log: .action, operationAwaitTime)
+
+        try? await Task.sleep(nanoseconds: UInt64(operationAwaitTime) * 1_000_000_000)
+
+        if let action = actionsHandler?.nextAction() {
+            os_log("Next action: %{public}@", log: .action, String(describing: action.actionType.rawValue))
+            await runNextAction(action)
+        } else {
+            os_log("Releasing the web view", log: .action)
+            await webViewHandler?.finish() // If we executed all steps we release the web view
+        }
+    }
+
+    func loadURL(url: URL) async {
+        do {
+            self.scanURL = url.absoluteString
+            try await webViewHandler?.load(url: url)
+            await executeNextStep()
+        } catch {
+            await completeWith(error: error)
+        }
+    }
+}
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBrokerProtectionProfile.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBrokerProtectionProfile.swift
index a77770d5f9..c76645a804 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBrokerProtectionProfile.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBrokerProtectionProfile.swift
@@ -69,7 +69,7 @@ public struct DataBrokerProtectionProfile: Codable {
     }
 }
 
-internal extension DataBrokerProtectionProfile {
+extension DataBrokerProtectionProfile {
     var profileQueries: [ProfileQuery] {
         return addresses.flatMap { address in
             names.map { name in
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ProfileQuery.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ProfileQuery.swift
index 0850e3027c..c2cbde5fa5 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ProfileQuery.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ProfileQuery.swift
@@ -117,6 +117,21 @@ extension ProfileQuery {
                              birthYear: birthYear,
                              deprecated: deprecated)
      }
+
+    func with(id: Int64) -> ProfileQuery {
+        return ProfileQuery(id: id,
+                            firstName: firstName,
+                            lastName: lastName,
+                            middleName: middleName,
+                            suffix: suffix,
+                            city: city,
+                            state: state,
+                            street: street,
+                            zipCode: zip,
+                            phone: phone,
+                            birthYear: birthYear,
+                            deprecated: deprecated)
+    }
 }
 
 extension ProfileQuery: Hashable {
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift
index ba59229aeb..7f12d1112f 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift
@@ -99,7 +99,7 @@ final class OptOutOperation: DataBrokerOperation {
         }
     }
 
-    func extractedProfiles(profiles: [ExtractedProfile]) async {
+    func extractedProfiles(profiles: [ExtractedProfile], meta: [String: Any]?) async {
         // No - op
     }
 
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift
index ac26510a64..06dfddd03d 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift
@@ -87,7 +87,7 @@ final class ScanOperation: DataBrokerOperation {
         }
     }
 
-    func extractedProfiles(profiles: [ExtractedProfile]) async {
+    func extractedProfiles(profiles: [ExtractedProfile], meta: [String: Any]?) async {
         complete(profiles)
         await executeNextStep()
     }
diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionFeatureTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionFeatureTests.swift
index 4fe2f44458..280b30050d 100644
--- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionFeatureTests.swift
+++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionFeatureTests.swift
@@ -118,7 +118,7 @@ final class MockCSSCommunicationDelegate: CCFCommunicationDelegate {
         self.url = url
     }
 
-    func extractedProfiles(profiles: [ExtractedProfile]) {
+    func extractedProfiles(profiles: [ExtractedProfile], meta: [String: Any]?) async {
         self.profiles = profiles
     }
 
diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift
index 24a55ee92d..cc36c1e9de 100644
--- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift
+++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift
@@ -214,6 +214,14 @@ final class WebViewHandlerMock: NSObject, WebViewHandler {
         wasExecuteJavascriptCalled = true
     }
 
+    func takeSnaphost(path: String, fileName: String) async throws {
+
+    }
+
+    func saveHTML(path: String, fileName: String) async throws {
+
+    }
+
     func reset() {
         wasInitializeWebViewCalled = false
         wasLoadCalledWithURL = nil

From 8f594baec13c6b48634fa0361bf7a7044a5d0ecd Mon Sep 17 00:00:00 2001
From: Dax the Duck <dax@duckduckgo.com>
Date: Fri, 15 Mar 2024 13:41:32 +0000
Subject: [PATCH 09/16] Update embedded files

---
 .../AppPrivacyConfigurationDataProvider.swift |  4 +-
 DuckDuckGo/ContentBlocker/macos-config.json   | 68 ++++++++++++++-----
 2 files changed, 52 insertions(+), 20 deletions(-)

diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift
index 427b2983c2..1843f8fc9d 100644
--- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift
+++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift
@@ -22,8 +22,8 @@ import BrowserServicesKit
 final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider {
 
     public struct Constants {
-        public static let embeddedDataETag = "\"b9487380b1490e67513c650e9497a28c\""
-        public static let embeddedDataSHA = "79590d4f2a9713b20eb127a29f862130fdc964145917004031022beaecf80fd0"
+        public static let embeddedDataETag = "\"11616ebbea5b6d7731bf08d224c6b1a7\""
+        public static let embeddedDataSHA = "980b19df068a45b754ed6865295960a914c3d2c3bcc2f20b9d1f8a5f2c1d68c3"
     }
 
     var embeddedDataEtag: String {
diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json
index 57da7cd02e..bb74d388c6 100644
--- a/DuckDuckGo/ContentBlocker/macos-config.json
+++ b/DuckDuckGo/ContentBlocker/macos-config.json
@@ -1,6 +1,6 @@
 {
     "readme": "https://github.com/duckduckgo/privacy-configuration",
-    "version": 1710170291349,
+    "version": 1710501855617,
     "features": {
         "adClickAttribution": {
             "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection",
@@ -264,6 +264,18 @@
                 {
                     "domain": "meneame.net"
                 },
+                {
+                    "domain": "espn.com"
+                },
+                {
+                    "domain": "usaa.com"
+                },
+                {
+                    "domain": "publico.es"
+                },
+                {
+                    "domain": "cnbc.com"
+                },
                 {
                     "domain": "earth.google.com"
                 },
@@ -287,7 +299,7 @@
                 ]
             },
             "state": "enabled",
-            "hash": "8458a9cc012a9613e1497204003be946"
+            "hash": "ec25d3a0b633fbc4f208f35999f4ab0e"
         },
         "autofill": {
             "exceptions": [
@@ -968,8 +980,11 @@
         },
         "clientBrandHint": {
             "exceptions": [],
+            "settings": {
+                "domains": []
+            },
             "state": "disabled",
-            "hash": "728493ef7a1488e4781656d3f9db84aa"
+            "hash": "d35dd75140cdfe166762013e59eb076d"
         },
         "contentBlocking": {
             "state": "enabled",
@@ -3904,6 +3919,23 @@
                             }
                         ]
                     },
+                    {
+                        "domain": "uzone.id",
+                        "rules": [
+                            {
+                                "selector": "[class^='box-ads']",
+                                "type": "hide-empty"
+                            },
+                            {
+                                "selector": "[class^='section-ads']",
+                                "type": "hide-empty"
+                            },
+                            {
+                                "selector": ".parallax-container",
+                                "type": "hide-empty"
+                            }
+                        ]
+                    },
                     {
                         "domain": "washingtontimes.com",
                         "rules": [
@@ -3984,7 +4016,7 @@
                 ]
             },
             "state": "enabled",
-            "hash": "3dc2d5b9a38827f46503a3b5882c7e33"
+            "hash": "3098b766cd343378237605f207f4fd17"
         },
         "exceptionHandler": {
             "exceptions": [
@@ -4878,6 +4910,16 @@
             "state": "enabled",
             "settings": {
                 "allowlistedTrackers": {
+                    "2mdn.net": {
+                        "rules": [
+                            {
+                                "rule": "2mdn.net",
+                                "domains": [
+                                    "crunchyroll.com"
+                                ]
+                            }
+                        ]
+                    },
                     "3lift.com": {
                         "rules": [
                             {
@@ -6013,22 +6055,11 @@
                     "fwmrm.net": {
                         "rules": [
                             {
-                                "rule": "2a7e9.v.fwmrm.net/ad/g/1",
+                                "rule": "v.fwmrm.net/ad",
                                 "domains": [
+                                    "6play.fr",
                                     "channel4.com"
                                 ]
-                            },
-                            {
-                                "rule": "2a7e9.v.fwmrm.net/ad/l/1",
-                                "domains": [
-                                    "channel4.com"
-                                ]
-                            },
-                            {
-                                "rule": "7cbf2.v.fwmrm.net/ad/g/1",
-                                "domains": [
-                                    "6play.fr"
-                                ]
                             }
                         ]
                     },
@@ -6197,6 +6228,7 @@
                                 "domains": [
                                     "arkadium.com",
                                     "bloomberg.com",
+                                    "crunchyroll.com",
                                     "gamak.tv",
                                     "games.washingtonpost.com",
                                     "metro.co.uk",
@@ -7946,7 +7978,7 @@
                     "domain": "sundancecatalog.com"
                 }
             ],
-            "hash": "4325ed151d126936fbeb9a608d77da86"
+            "hash": "1bbefc586e08d2c42b5db17ed95cc8e5"
         },
         "trackingCookies1p": {
             "settings": {

From ecdd9ac00618cdf9cb3314be0f86548dc9f37855 Mon Sep 17 00:00:00 2001
From: Dax the Duck <dax@duckduckgo.com>
Date: Fri, 15 Mar 2024 13:41:32 +0000
Subject: [PATCH 10/16] Set marketing version to 1.80.0

---
 Configuration/Version.xcconfig | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig
index da3c8e7045..7a0581611b 100644
--- a/Configuration/Version.xcconfig
+++ b/Configuration/Version.xcconfig
@@ -1 +1 @@
-MARKETING_VERSION = 1.79.0
+MARKETING_VERSION = 1.80.0

From 05d80b76af5b65443a8dcf923a25e659388221b5 Mon Sep 17 00:00:00 2001
From: Dax the Duck <dax@duckduckgo.com>
Date: Fri, 15 Mar 2024 13:52:33 +0000
Subject: [PATCH 11/16] Bump version to 1.80.0 (144)

---
 Configuration/BuildNumber.xcconfig | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig
index 9a14499d7a..db702d8aab 100644
--- a/Configuration/BuildNumber.xcconfig
+++ b/Configuration/BuildNumber.xcconfig
@@ -1 +1 @@
-CURRENT_PROJECT_VERSION = 143
+CURRENT_PROJECT_VERSION = 144

From 8fdbc689ac386571c186ff2f69a67c5d1e051b83 Mon Sep 17 00:00:00 2001
From: Dominik Kapusta <dkapusta@duckduckgo.com>
Date: Fri, 15 Mar 2024 15:08:14 +0100
Subject: [PATCH 12/16] Fix syntax in Asana actions

---
 .github/actions/asana-create-action-item/action.yml | 2 +-
 .github/actions/asana-log-message/action.yml        | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/actions/asana-create-action-item/action.yml b/.github/actions/asana-create-action-item/action.yml
index f3c5455006..7f4ee3f3c2 100644
--- a/.github/actions/asana-create-action-item/action.yml
+++ b/.github/actions/asana-create-action-item/action.yml
@@ -44,7 +44,7 @@ runs:
         task-url: ${{ inputs.release-task-url }}
 
     - id: get-asana-user-id
-      if: github.event_name != "schedule"
+      if: github.event_name != 'schedule'
       uses: duckduckgo/apple-infra/actions/asana-get-user-id-for-github-handle@main
       with:
         access-token: ${{ inputs.access-token }}
diff --git a/.github/actions/asana-log-message/action.yml b/.github/actions/asana-log-message/action.yml
index 86940852bf..288fd832ba 100644
--- a/.github/actions/asana-log-message/action.yml
+++ b/.github/actions/asana-log-message/action.yml
@@ -30,7 +30,7 @@ runs:
         task-url: ${{ inputs.task-url }}
 
     - id: get-asana-user-id
-      if: github.event_name != "schedule"
+      if: github.event_name != 'schedule'
       uses: duckduckgo/apple-infra/actions/asana-get-user-id-for-github-handle@main
       with:
         access-token: ${{ inputs.access-token }}

From 90cff6bc14ff3f80997ad1251d37464397b3bcf7 Mon Sep 17 00:00:00 2001
From: Dax the Duck <dax@duckduckgo.com>
Date: Fri, 15 Mar 2024 14:28:41 +0000
Subject: [PATCH 13/16] Bump version to 1.80.0 (145)

---
 Configuration/BuildNumber.xcconfig | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig
index db702d8aab..b11c07fc54 100644
--- a/Configuration/BuildNumber.xcconfig
+++ b/Configuration/BuildNumber.xcconfig
@@ -1 +1 @@
-CURRENT_PROJECT_VERSION = 144
+CURRENT_PROJECT_VERSION = 145

From 793d16e629d10198642340da0ad0530fd0448845 Mon Sep 17 00:00:00 2001
From: Daniel Bernal <dbernal@duckduckgo.com>
Date: Sat, 16 Mar 2024 00:35:41 +0100
Subject: [PATCH 14/16] Bump BSK (#2435)

Task/Issue URL:
https://app.asana.com/0/72649045549333/1206350282230222/f

**Description**:
Bump BSK

**Steps to test this PR**:
1. iOS Changes Only

<!--
Tagging instructions
If this PR isn't ready to be merged for whatever reason it should be
marked with the `DO NOT MERGE` label (particularly if it's a draft)
If it's pending Product Review/PFR, please add the `Pending Product
Review` label.

If at any point it isn't actively being worked on/ready for
review/otherwise moving forward (besides the above PR/PFR exception)
strongly consider closing it (or not opening it in the first place). If
you decide not to close it, make sure it's labelled to make it clear the
PRs state and comment with more information.
-->

---
###### 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)
---
 DuckDuckGo.xcodeproj/project.pbxproj                          | 2 +-
 .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++--
 LocalPackages/DataBrokerProtection/Package.swift              | 2 +-
 LocalPackages/NetworkProtectionMac/Package.swift              | 2 +-
 LocalPackages/SubscriptionUI/Package.swift                    | 2 +-
 5 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj
index 23d4cdecf1..c985be5176 100644
--- a/DuckDuckGo.xcodeproj/project.pbxproj
+++ b/DuckDuckGo.xcodeproj/project.pbxproj
@@ -13749,7 +13749,7 @@
 			repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit";
 			requirement = {
 				kind = exactVersion;
-				version = 124.1.0;
+				version = 125.0.0;
 			};
 		};
 		AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 5d9e06d99f..24e42fd976 100644
--- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -32,8 +32,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/duckduckgo/BrowserServicesKit",
       "state" : {
-        "revision" : "bcafd206465427c560f9f581def57d8eef53748c",
-        "version" : "124.1.0"
+        "revision" : "ac2127a26f75b2aa293f6036bcdd2bc241d09819",
+        "version" : "125.0.0"
       }
     },
     {
diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift
index e07c2c873c..d428da60b2 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: "124.1.0"),
+        .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.0"),
         .package(path: "../PixelKit"),
         .package(path: "../SwiftUIExtensions"),
         .package(path: "../XPCHelper"),
diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift
index 5c07540155..39f13d9fe0 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: "124.1.0"),
+        .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.0"),
         .package(path: "../XPCHelper"),
         .package(path: "../SwiftUIExtensions"),
         .package(path: "../LoginItems"),
diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift
index c29571c2ce..6ffb49191b 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: "124.1.0"),
+        .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.0"),
         .package(path: "../SwiftUIExtensions")
     ],
     targets: [

From 5d7b4bc272c7f06188e129f807d44dc99f5b6894 Mon Sep 17 00:00:00 2001
From: Juan Manuel Pereira <juanmanuel.pereira1@gmail.com>
Date: Sun, 17 Mar 2024 13:50:36 -0300
Subject: [PATCH 15/16] DBP: Implement event pixels (#2408)

---
 DuckDuckGo/DBP/DBPHomeViewController.swift    |   6 +-
 .../Model/HistoryEvent.swift                  |  10 +
 ...taBrokerProfileQueryOperationManager.swift |   7 +-
 .../OperationPreferredDateCalculator.swift    |   5 +-
 .../DataBrokerProtectionEventPixels.swift     | 142 ++++++++
 .../Pixels/DataBrokerProtectionPixels.swift   |  29 +-
 .../DataBrokerProtectionProcessor.swift       |   4 +
 ...kerProfileQueryOperationManagerTests.swift |  17 +
 ...DataBrokerProtectionEventPixelsTests.swift | 318 ++++++++++++++++++
 9 files changed, 531 insertions(+), 7 deletions(-)
 create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift
 create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift

diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift
index b64fc689e8..9313c0530a 100644
--- a/DuckDuckGo/DBP/DBPHomeViewController.swift
+++ b/DuckDuckGo/DBP/DBPHomeViewController.swift
@@ -191,7 +191,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping<DataBrokerProtectio
                     .dataBrokerProtectionNotificationOpenedAllRecordsRemoved,
                     .dailyActiveUser,
                     .weeklyActiveUser,
-                    .monthlyActiveUser:
+                    .monthlyActiveUser,
+                    .weeklyReportScanning,
+                    .weeklyReportRemovals,
+                    .scanningEventNewMatch,
+                    .scanningEventReAppearance:
                 Pixel.fire(.pixelKitEvent(event))
             }
         }
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/HistoryEvent.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/HistoryEvent.swift
index 111d6dad23..9ed693822b 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/HistoryEvent.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/HistoryEvent.swift
@@ -27,6 +27,7 @@ public struct HistoryEvent: Identifiable, Sendable {
         case optOutRequested
         case optOutConfirmed
         case scanStarted
+        case reAppearence
     }
 
     public let extractedProfileId: Int64?
@@ -50,4 +51,13 @@ public struct HistoryEvent: Identifiable, Sendable {
         self.date = date
         self.type = type
     }
+
+    func matchesFound() -> Int {
+        switch type {
+        case .matchesFound(let matchesFound):
+            return matchesFound
+        default:
+            return 0
+        }
+    }
 }
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift
index 58352c3fc3..f5c29ae8b7 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift
@@ -117,7 +117,8 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager {
             notificationCenter.post(name: DataBrokerProtectionNotifications.didFinishScan, object: brokerProfileQueryData.dataBroker.name)
         }
 
-        let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.url, handler: pixelHandler)
+        let eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler)
+        let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, handler: pixelHandler)
 
         do {
             let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .scanStarted)
@@ -141,6 +142,9 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager {
                     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 {
+                            let reAppereanceEvent = HistoryEvent(extractedProfileId: extractedProfile.id, brokerId: brokerId, profileQueryId: profileQueryId, type: .reAppearence)
+                            eventPixels.fireReAppereanceEventPixel()
+                            database.add(reAppereanceEvent)
                             database.updateRemovedDate(nil, on: id)
                         }
 
@@ -148,6 +152,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager {
                     } else {
                         // If it's a new found profile, we'd like to opt-out ASAP
                         // If this broker has a parent opt out, we set the preferred date to nil, as we will only perform the operation within the parent.
+                        eventPixels.fireNewMatchEventPixel()
                         let broker = brokerProfileQueryData.dataBroker
                         let preferredRunOperation: Date? = broker.performsOptOutWithinParent() ? nil : Date()
 
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift
index f5d2063be0..ca42eaedda 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateCalculator.swift
@@ -41,14 +41,13 @@ struct OperationPreferredDateCalculator {
         }
 
         switch lastEvent.type {
-
         case .optOutConfirmed:
             if isDeprecated {
                 return nil
             } else {
                 return Date().addingTimeInterval(schedulingConfig.maintenanceScan.hoursToSeconds)
             }
-        case .noMatchFound, .matchesFound:
+        case .noMatchFound, .matchesFound, .reAppearence:
             return Date().addingTimeInterval(schedulingConfig.maintenanceScan.hoursToSeconds)
         case .error:
             return Date().addingTimeInterval(schedulingConfig.retryError.hoursToSeconds)
@@ -70,7 +69,7 @@ struct OperationPreferredDateCalculator {
         }
 
         switch lastEvent.type {
-        case .matchesFound:
+        case .matchesFound, .reAppearence:
             if let extractedProfileID = extractedProfileID, shouldScheduleNewOptOut(events: historyEvents,
                                                                                     extractedProfileId: extractedProfileID,
                                                                                     schedulingConfig: schedulingConfig) {
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift
new file mode 100644
index 0000000000..fd0a382703
--- /dev/null
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift
@@ -0,0 +1,142 @@
+//
+//  DataBrokerProtectionEventPixels.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 Common
+import BrowserServicesKit
+import PixelKit
+
+protocol DataBrokerProtectionEventPixelsRepository {
+    func markWeeklyPixelSent()
+
+    func getLatestWeeklyPixel() -> Date?
+}
+
+final class DataBrokerProtectionEventPixelsUserDefaults: DataBrokerProtectionEventPixelsRepository {
+
+    enum Consts {
+        static let weeklyPixelKey = "macos.browser.data-broker-protection.eventsWeeklyPixelKey"
+    }
+
+    private let userDefaults: UserDefaults
+
+    init(userDefaults: UserDefaults = .standard) {
+        self.userDefaults = userDefaults
+    }
+
+    func markWeeklyPixelSent() {
+        userDefaults.set(Date(), forKey: Consts.weeklyPixelKey)
+    }
+
+    func getLatestWeeklyPixel() -> Date? {
+        userDefaults.object(forKey: Consts.weeklyPixelKey) as? Date
+    }
+}
+
+final class DataBrokerProtectionEventPixels {
+
+    private let database: DataBrokerProtectionRepository
+    private let repository: DataBrokerProtectionEventPixelsRepository
+    private let handler: EventMapping<DataBrokerProtectionPixels>
+    private let calendar = Calendar.current
+
+    init(database: DataBrokerProtectionRepository,
+         repository: DataBrokerProtectionEventPixelsRepository = DataBrokerProtectionEventPixelsUserDefaults(),
+         handler: EventMapping<DataBrokerProtectionPixels>) {
+        self.database = database
+        self.repository = repository
+        self.handler = handler
+    }
+
+    func tryToFireWeeklyPixels() {
+        if shouldWeFireWeeklyPixel() {
+            fireWeeklyReportPixels()
+            repository.markWeeklyPixelSent()
+        }
+    }
+
+    func fireNewMatchEventPixel() {
+        handler.fire(.scanningEventNewMatch)
+    }
+
+    func fireReAppereanceEventPixel() {
+        handler.fire(.scanningEventReAppearance)
+    }
+
+    private func shouldWeFireWeeklyPixel() -> Bool {
+        guard let lastPixelFiredDate = repository.getLatestWeeklyPixel() else {
+            return true // Last pixel fired date is not present. We should fire it
+        }
+
+        return didWeekPassedBetweenDates(start: lastPixelFiredDate, end: Date())
+    }
+
+    private func fireWeeklyReportPixels() {
+        let data = database.fetchAllBrokerProfileQueryData()
+        let dataInThePastWeek = data.filter(hadScanThisWeek(_:))
+
+        var newMatchesFoundInTheLastWeek = 0
+        var reAppereancesInTheLastWeek = 0
+        var removalsInTheLastWeek = 0
+
+        for query in data {
+            let allHistoryEventsForQuery = query.scanOperationData.historyEvents + query.optOutOperationsData.flatMap { $0.historyEvents }
+            let historyEventsInThePastWeek = allHistoryEventsForQuery.filter {
+                !didWeekPassedBetweenDates(start: $0.date, end: Date())
+            }
+            let newMatches = historyEventsInThePastWeek.reduce(0, { result, next in
+                return result + next.matchesFound()
+            })
+            let reAppereances = historyEventsInThePastWeek.filter { $0.type == .reAppearence }.count
+            let removals = historyEventsInThePastWeek.filter { $0.type == .optOutConfirmed }.count
+
+            newMatchesFoundInTheLastWeek += newMatches
+            reAppereancesInTheLastWeek += reAppereances
+            removalsInTheLastWeek += removals
+        }
+
+        let totalBrokers = Dictionary(grouping: data, by: { $0.dataBroker.url }).count
+        let totalBrokersInTheLastWeek = Dictionary(grouping: dataInThePastWeek, by: { $0.dataBroker.url }).count
+        var percentageOfBrokersScanned: Int
+
+        if totalBrokers == 0 {
+            percentageOfBrokersScanned = 0
+        } else {
+            percentageOfBrokersScanned = (totalBrokersInTheLastWeek * 100) / totalBrokers
+        }
+
+        handler.fire(.weeklyReportScanning(hadNewMatch: newMatchesFoundInTheLastWeek > 0, hadReAppereance: reAppereancesInTheLastWeek > 0, scanCoverage: percentageOfBrokersScanned))
+        handler.fire(.weeklyReportRemovals(removals: removalsInTheLastWeek))
+    }
+
+    private func hadScanThisWeek(_ brokerProfileQuery: BrokerProfileQueryData) -> Bool {
+        return brokerProfileQuery.scanOperationData.historyEvents.contains { historyEvent in
+            !didWeekPassedBetweenDates(start: historyEvent.date, end: Date())
+        }
+    }
+
+    private func didWeekPassedBetweenDates(start: Date, end: Date) -> Bool {
+        let components = calendar.dateComponents([.day], from: start, to: end)
+
+        if let differenceInDays = components.day {
+            return differenceInDays >= 7
+        } else {
+            return false
+        }
+    }
+}
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift
index e7198fe713..9663bbae4d 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift
@@ -54,6 +54,10 @@ public enum DataBrokerProtectionPixels {
         static let pattern = "pattern"
         static let isParent = "is_parent"
         static let actionIDKey = "action_id"
+        static let hadNewMatch = "had_new_match"
+        static let hadReAppereance = "had_re-appearance"
+        static let scanCoverage = "scan_coverage"
+        static let removals = "removals"
     }
 
     case error(error: DataBrokerProtectionError, dataBroker: String)
@@ -117,6 +121,12 @@ public enum DataBrokerProtectionPixels {
     case dailyActiveUser
     case weeklyActiveUser
     case monthlyActiveUser
+
+    // KPIs - events
+    case weeklyReportScanning(hadNewMatch: Bool, hadReAppereance: Bool, scanCoverage: Int)
+    case weeklyReportRemovals(removals: Int)
+    case scanningEventNewMatch
+    case scanningEventReAppearance
 }
 
 extension DataBrokerProtectionPixels: PixelKitEvent {
@@ -190,6 +200,11 @@ extension DataBrokerProtectionPixels: PixelKitEvent {
         case .dailyActiveUser: return "m_mac_dbp_engagement_dau"
         case .weeklyActiveUser: return "m_mac_dbp_engagement_wau"
         case .monthlyActiveUser: return "m_mac_dbp_engagement_mau"
+
+        case .weeklyReportScanning: return "m_mac_dbp_event_weekly-report_scanning"
+        case .weeklyReportRemovals: return "m_mac_dbp_event_weekly-report_removals"
+        case .scanningEventNewMatch: return "m_mac_dbp_event_scanning-events_new-match"
+        case .scanningEventReAppearance: return "m_mac_dbp_event_scanning-events_re-appearance"
         }
     }
 
@@ -251,6 +266,10 @@ extension DataBrokerProtectionPixels: PixelKitEvent {
             }
 
             return params
+        case .weeklyReportScanning(let hadNewMatch, let hadReAppereance, let scanCoverage):
+            return [Consts.hadNewMatch: hadNewMatch.description, Consts.hadReAppereance: hadReAppereance.description, Consts.scanCoverage: scanCoverage.description]
+        case .weeklyReportRemovals(let removals):
+            return [Consts.removals: String(removals)]
         case .backgroundAgentStarted,
                 .backgroundAgentRunOperationsAndStartSchedulerIfPossible,
                 .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile,
@@ -266,7 +285,9 @@ extension DataBrokerProtectionPixels: PixelKitEvent {
                 .dataBrokerProtectionNotificationOpenedAllRecordsRemoved,
                 .dailyActiveUser,
                 .weeklyActiveUser,
-                .monthlyActiveUser:
+                .monthlyActiveUser,
+                .scanningEventNewMatch,
+                .scanningEventReAppearance:
             return [:]
         case .ipcServerRegister,
                 .ipcServerStartScheduler,
@@ -341,7 +362,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping<DataBrokerProtectio
                     .dataBrokerProtectionNotificationOpenedAllRecordsRemoved,
                     .dailyActiveUser,
                     .weeklyActiveUser,
-                    .monthlyActiveUser:
+                    .monthlyActiveUser,
+                    .weeklyReportScanning,
+                    .weeklyReportRemovals,
+                    .scanningEventNewMatch,
+                    .scanningEventReAppearance:
 
                 PixelKit.fire(event)
             }
diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift
index a06a9a3b92..d6971d1a80 100644
--- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift
+++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift
@@ -33,6 +33,7 @@ final class DataBrokerProtectionProcessor {
     private var pixelHandler: EventMapping<DataBrokerProtectionPixels>
     private let userNotificationService: DataBrokerProtectionUserNotificationService
     private let engagementPixels: DataBrokerProtectionEngagementPixels
+    private let eventPixels: DataBrokerProtectionEventPixels
 
     init(database: DataBrokerProtectionRepository,
          config: SchedulerConfig,
@@ -50,6 +51,7 @@ final class DataBrokerProtectionProcessor {
         self.operationQueue.maxConcurrentOperationCount = config.concurrentOperationsDifferentBrokers
         self.userNotificationService = userNotificationService
         self.engagementPixels = DataBrokerProtectionEngagementPixels(database: database, handler: pixelHandler)
+        self.eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler)
     }
 
     // MARK: - Public functions
@@ -115,6 +117,8 @@ final class DataBrokerProtectionProcessor {
 
         // This will fire the DAU/WAU/MAU pixels,
         engagementPixels.fireEngagementPixel()
+        // This will try to fire the event weekly report pixels
+        eventPixels.tryToFireWeeklyPixels()
 
         let brokersProfileData = database.fetchAllBrokerProfileQueryData()
         let dataBrokerOperationCollections = createDataBrokerOperationCollections(from: brokersProfileData,
diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift
index 08796454c7..70619e8736 100644
--- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift
+++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift
@@ -974,6 +974,10 @@ extension ScanOperationData {
             historyEvents: [HistoryEvent]()
         )
     }
+
+    static func mockWith(historyEvents: [HistoryEvent]) -> ScanOperationData {
+        ScanOperationData(brokerId: 1, profileQueryId: 1, historyEvents: historyEvents)
+    }
 }
 
 extension OptOutOperationData {
@@ -1035,6 +1039,19 @@ extension DataBroker {
             )
         )
     }
+
+    static func mockWithURL(_ url: String) -> DataBroker {
+        .init(name: "Test",
+              url: url,
+              steps: [Step](),
+              version: "1.0",
+              schedulingConfig: DataBrokerScheduleConfig(
+                retryError: 0,
+                confirmOptOutScan: 0,
+                maintenanceScan: 0
+            )
+        )
+    }
 }
 
 extension ProfileQuery {
diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift
new file mode 100644
index 0000000000..4c5a93563b
--- /dev/null
+++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEventPixelsTests.swift
@@ -0,0 +1,318 @@
+//
+//  DataBrokerProtectionEventPixelsTests.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 DataBrokerProtectionEventPixelsTests: XCTestCase {
+
+    let database = MockDatabase()
+    let repository = MockDataBrokerProtectionEventPixelsRepository()
+    let handler = MockDataBrokerProtectionPixelsHandler()
+    let calendar = Calendar.current
+
+    override func tearDown() {
+        handler.clear()
+        repository.clear()
+    }
+
+    func testWhenFireNewMatchEventPixelIsCalled_thenCorrectPixelIsFired() {
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+
+        sut.fireNewMatchEventPixel()
+
+        XCTAssertEqual(
+            MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last!.name,
+            DataBrokerProtectionPixels.scanningEventNewMatch.name
+        )
+    }
+
+    func testWhenFireReAppereanceEventPixelIsCalled_thenCorrectPixelIsFired() {
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+
+        sut.fireReAppereanceEventPixel()
+
+        XCTAssertEqual(
+            MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last!.name,
+            DataBrokerProtectionPixels.scanningEventReAppearance.name
+        )
+    }
+
+    func testWhenReportWasFiredInTheLastWeek_thenWeDoNotFireWeeklyPixels() {
+        repository.customGetLatestWeeklyPixel = Date().yesterday
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+
+        sut.tryToFireWeeklyPixels()
+
+        XCTAssertFalse(repository.wasMarkWeeklyPixelSentCalled)
+    }
+
+    func testWhenReportWasNotFiredInTheLastWeek_thenWeFireWeeklyPixels() {
+        guard let eightDaysSinceToday = Calendar.current.date(byAdding: .day, value: -8, to: Date()) else {
+            XCTFail("This should no throw")
+            return
+        }
+
+        repository.customGetLatestWeeklyPixel = eightDaysSinceToday
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+
+        sut.tryToFireWeeklyPixels()
+
+        XCTAssertTrue(repository.wasMarkWeeklyPixelSentCalled)
+    }
+
+    func testWhenLastWeeklyPixelIsNil_thenWeFireWeeklyPixels() {
+        repository.customGetLatestWeeklyPixel = nil
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+
+        sut.tryToFireWeeklyPixels()
+
+        XCTAssertTrue(repository.wasMarkWeeklyPixelSentCalled)
+    }
+
+    func testWhenReAppereanceOcurredInTheLastWeek_thenReAppereanceFlagIsTrue() {
+        guard let sixDaysSinceToday = Calendar.current.date(byAdding: .day, value: -6, to: Date()) else {
+            XCTFail("This should no throw")
+            return
+        }
+
+        let reAppereanceThisWeekEvent = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .reAppearence, date: sixDaysSinceToday)
+        let dataBrokerProfileQueryWithReAppereance: [BrokerProfileQueryData] = [
+            .init(dataBroker: .mock,
+                  profileQuery: .mock,
+                  scanOperationData: .mockWith(historyEvents: [reAppereanceThisWeekEvent]))
+        ]
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+        database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithReAppereance
+        repository.customGetLatestWeeklyPixel = nil
+
+        sut.tryToFireWeeklyPixels()
+
+        let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!
+        let hadReAppereance = weeklyReportScanningPixel.params!["had_re-appearance"]!
+
+        XCTAssertTrue(Bool(hadReAppereance)!)
+    }
+
+    func testWhenReAppereanceDidNotOcurrInTheLastWeek_thenReAppereanceFlagIsFalse() {
+        guard let eighDaysSinceToday = Calendar.current.date(byAdding: .day, value: -8, to: Date()) else {
+            XCTFail("This should no throw")
+            return
+        }
+
+        let reAppereanceThisWeekEvent = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .reAppearence, date: eighDaysSinceToday)
+        let dataBrokerProfileQueryWithReAppereance: [BrokerProfileQueryData] = [
+            .init(dataBroker: .mock,
+                  profileQuery: .mock,
+                  scanOperationData: .mockWith(historyEvents: [reAppereanceThisWeekEvent]))
+        ]
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+        database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithReAppereance
+        repository.customGetLatestWeeklyPixel = nil
+
+        sut.tryToFireWeeklyPixels()
+
+        let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!
+        let hadReAppereance = weeklyReportScanningPixel.params!["had_re-appearance"]!
+
+        XCTAssertFalse(Bool(hadReAppereance)!)
+    }
+
+    func testWhenNoMatchesHappendInTheLastWeek_thenHadNewMatchFlagIsFalse() {
+        guard let eighDaysSinceToday = Calendar.current.date(byAdding: .day, value: -8, to: Date()) else {
+            XCTFail("This should no throw")
+            return
+        }
+
+        let newMatchesPriorToThisWeekEvent = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 2), date: eighDaysSinceToday)
+        let dataBrokerProfileQueryWithMatches: [BrokerProfileQueryData] = [
+            .init(dataBroker: .mock,
+                  profileQuery: .mock,
+                  scanOperationData: .mockWith(historyEvents: [newMatchesPriorToThisWeekEvent]))
+        ]
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+        database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithMatches
+        repository.customGetLatestWeeklyPixel = nil
+
+        sut.tryToFireWeeklyPixels()
+
+        let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!
+        let hadNewMatch = weeklyReportScanningPixel.params!["had_new_match"]!
+
+        XCTAssertFalse(Bool(hadNewMatch)!)
+    }
+
+    func testWhenMatchesHappendInTheLastWeek_thenHadNewMatchFlagIsTrue() {
+        guard let sixDaysSinceToday = Calendar.current.date(byAdding: .day, value: -6, to: Date()) else {
+            XCTFail("This should no throw")
+            return
+        }
+
+        let newMatchesThisWeekEvent = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 2), date: sixDaysSinceToday)
+        let dataBrokerProfileQueryWithMatches: [BrokerProfileQueryData] = [
+            .init(dataBroker: .mock,
+                  profileQuery: .mock,
+                  scanOperationData: .mockWith(historyEvents: [newMatchesThisWeekEvent]))
+        ]
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+        database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithMatches
+        repository.customGetLatestWeeklyPixel = nil
+
+        sut.tryToFireWeeklyPixels()
+
+        let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!
+        let hadNewMatch = weeklyReportScanningPixel.params!["had_new_match"]!
+
+        XCTAssertTrue(Bool(hadNewMatch)!)
+    }
+
+    func testWhenNoRemovalsHappendInTheLastWeek_thenRemovalsCountIsZero() {
+        guard let eighDaysSinceToday = Calendar.current.date(byAdding: .day, value: -8, to: Date()) else {
+            XCTFail("This should no throw")
+            return
+        }
+
+        let removalsPriorToThisWeekEvent = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .optOutConfirmed, date: eighDaysSinceToday)
+        let dataBrokerProfileQueryWithRemovals: [BrokerProfileQueryData] = [
+            .init(dataBroker: .mock,
+                  profileQuery: .mock,
+                  scanOperationData: .mockWith(historyEvents: [removalsPriorToThisWeekEvent]))
+        ]
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+        database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithRemovals
+        repository.customGetLatestWeeklyPixel = nil
+
+        sut.tryToFireWeeklyPixels()
+
+        let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last!
+        let removals = weeklyReportScanningPixel.params!["removals"]!
+
+        XCTAssertEqual("0", removals)
+    }
+
+    func testWhenRemovalsHappendInTheLastWeek_thenRemovalsCountIsCorrect() {
+        guard let sixDaysSinceToday = Calendar.current.date(byAdding: .day, value: -6, to: Date()) else {
+            XCTFail("This should no throw")
+            return
+        }
+
+        let removalThisWeekEventOne = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .optOutConfirmed, date: sixDaysSinceToday)
+        let removalThisWeekEventTwo = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .optOutConfirmed, date: sixDaysSinceToday)
+        let dataBrokerProfileQueryWithRemovals: [BrokerProfileQueryData] = [
+            .init(dataBroker: .mock,
+                  profileQuery: .mock,
+                  scanOperationData: .mockWith(historyEvents: [removalThisWeekEventOne, removalThisWeekEventTwo]))
+        ]
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+        database.brokerProfileQueryDataToReturn = dataBrokerProfileQueryWithRemovals
+        repository.customGetLatestWeeklyPixel = nil
+
+        sut.tryToFireWeeklyPixels()
+
+        let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last!
+        let removals = weeklyReportScanningPixel.params!["removals"]!
+
+        XCTAssertEqual("2", removals)
+    }
+
+    func testWhenNoHistoryEventsHappenedInTheLastWeek_thenBrokersScannedIsZero() {
+        guard let eighDaysSinceToday = Calendar.current.date(byAdding: .day, value: -8, to: Date()) else {
+            XCTFail("This should no throw")
+            return
+        }
+
+        let eventOne = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .optOutStarted, date: eighDaysSinceToday)
+        let eventTwo = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .error(error: .cancelled), date: eighDaysSinceToday)
+        let eventThree = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: eighDaysSinceToday)
+        let eventFour = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .noMatchFound, date: eighDaysSinceToday)
+        let dataBrokerProfileQueries: [BrokerProfileQueryData] = [
+            .init(dataBroker: .mock,
+                  profileQuery: .mock,
+                  scanOperationData: .mockWith(historyEvents: [eventOne, eventTwo, eventThree, eventFour]))
+        ]
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+        database.brokerProfileQueryDataToReturn = dataBrokerProfileQueries
+        repository.customGetLatestWeeklyPixel = nil
+
+        sut.tryToFireWeeklyPixels()
+
+        let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!
+        let scanCoverage = weeklyReportScanningPixel.params!["scan_coverage"]!
+
+        XCTAssertEqual("0", scanCoverage)
+    }
+
+    func testWhenHistoryEventsHappenedInTheLastWeek_thenBrokersScannedIsMoreThanZero() {
+        guard let eighDaysSinceToday = Calendar.current.date(byAdding: .day, value: -8, to: Date()) else {
+            XCTFail("This should no throw")
+            return
+        }
+
+        guard let sixDaysSinceToday = Calendar.current.date(byAdding: .day, value: -6, to: Date()) else {
+            XCTFail("This should no throw")
+            return
+        }
+
+        let eventOne = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: eighDaysSinceToday)
+        let eventTwo = HistoryEvent(brokerId: 1, profileQueryId: 1, type: .scanStarted, date: sixDaysSinceToday)
+        let dataBrokerProfileQueries: [BrokerProfileQueryData] = [
+            .init(dataBroker: .mockWithURL("www.brokerone.com"),
+                  profileQuery: .mock,
+                  scanOperationData: .mockWith(historyEvents: [eventOne])),
+            .init(dataBroker: .mockWithURL("www.brokertwo.com"),
+                  profileQuery: .mock,
+                  scanOperationData: .mockWith(historyEvents: [eventOne])),
+            .init(dataBroker: .mockWithURL("www.brokerthree.com"),
+                  profileQuery: .mock,
+                  scanOperationData: .mockWith(historyEvents: [eventTwo])),
+            .init(dataBroker: .mockWithURL("www.brokerfour.com"),
+                  profileQuery: .mock,
+                  scanOperationData: .mockWith(historyEvents: [eventTwo]))
+        ]
+        let sut = DataBrokerProtectionEventPixels(database: database, repository: repository, handler: handler)
+        database.brokerProfileQueryDataToReturn = dataBrokerProfileQueries
+        repository.customGetLatestWeeklyPixel = nil
+
+        sut.tryToFireWeeklyPixels()
+
+        let weeklyReportScanningPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!
+        let scanCoverage = weeklyReportScanningPixel.params!["scan_coverage"]!
+
+        XCTAssertEqual("50", scanCoverage)
+    }
+}
+
+final class MockDataBrokerProtectionEventPixelsRepository: DataBrokerProtectionEventPixelsRepository {
+
+    var wasMarkWeeklyPixelSentCalled = false
+    var customGetLatestWeeklyPixel: Date?
+
+    func markWeeklyPixelSent() {
+        wasMarkWeeklyPixelSentCalled = true
+    }
+
+    func getLatestWeeklyPixel() -> Date? {
+        return customGetLatestWeeklyPixel
+    }
+
+    func clear() {
+        wasMarkWeeklyPixelSentCalled = false
+        customGetLatestWeeklyPixel = nil
+    }
+}

From 67c9d66e68e911cb80fcd368af55c5389180af5f Mon Sep 17 00:00:00 2001
From: Diego Rey Mendez <diegoreymendez@users.noreply.github.com>
Date: Mon, 18 Mar 2024 00:29:02 +0100
Subject: [PATCH 16/16] Improves VPN pixel info and adds tests (#2434)

Task/Issue URL: https://app.asana.com/0/0/1206857816167860/f

iOS: https://github.com/duckduckgo/iOS/pull/2604
BSK: https://github.com/duckduckgo/BrowserServicesKit/pull/732

## Description

1. Makes `NetworkProtectionPixelEvent` implement `PixelKitEventV2` to
make it easier for me to add automated tests.
2. Ensures all our error pixels contain both error and underlying error
information.
3. Adds automated to validate that all pixels in
`NetworkProtectionPixelEvent` send the right information.
---
 DuckDuckGo.xcodeproj/project.pbxproj          |   8 +-
 .../xcshareddata/swiftpm/Package.resolved     |   8 +-
 .../DuckDuckGo Privacy Browser.xcscheme       |   3 +
 .../NetworkProtectionPixelEvent.swift         | 210 +++++++----
 .../EventMapping+NetworkProtectionError.swift |   8 +-
 .../NetworkProtectionTunnelController.swift   |   2 +-
 .../MacPacketTunnelProvider.swift             |  23 +-
 .../DataBrokerProtection/Package.swift        |   2 +-
 .../NetworkProtectionMac/Package.swift        |   2 +-
 .../PixelKit/PixelKit+Parameters.swift        |   2 +-
 .../PixelFireExpectations.swift               |  28 +-
 .../XCTestCase+PixelKit.swift                 |  42 +--
 LocalPackages/SubscriptionUI/Package.swift    |   2 +-
 .../NetworkProtectionPixelEventTests.swift    | 345 ++++++++++++++++++
 14 files changed, 563 insertions(+), 122 deletions(-)
 create mode 100644 UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift

diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj
index c985be5176..9f4d19a784 100644
--- a/DuckDuckGo.xcodeproj/project.pbxproj
+++ b/DuckDuckGo.xcodeproj/project.pbxproj
@@ -2171,6 +2171,8 @@
 		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 */; };
+		7B09CBA92BA4BE8100CF245B /* NetworkProtectionPixelEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */; };
+		7B09CBAA2BA4BE8200CF245B /* NetworkProtectionPixelEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.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 */; };
@@ -3833,6 +3835,7 @@
 		7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTransparentProxyProvider.swift; sourceTree = "<group>"; };
 		7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = "<group>"; };
 		7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNProxyLauncher.swift; sourceTree = "<group>"; };
+		7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionPixelEventTests.swift; sourceTree = "<group>"; };
 		7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayPopover.swift; sourceTree = "<group>"; };
 		7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ContentOverlay.storyboard; sourceTree = "<group>"; };
 		7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayViewController.swift; sourceTree = "<group>"; };
@@ -6095,6 +6098,7 @@
 				4BCF15E62ABB98A20083F6DF /* Resources */,
 				4BCF15E42ABB98990083F6DF /* NetworkProtectionRemoteMessageTests.swift */,
 				4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */,
+				7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */,
 			);
 			path = NetworkProtection;
 			sourceTree = "<group>";
@@ -10702,6 +10706,7 @@
 				3706FE78293F661700E42796 /* HistoryCoordinatingMock.swift in Sources */,
 				3706FE79293F661700E42796 /* AppearancePreferencesTests.swift in Sources */,
 				3706FE7A293F661700E42796 /* FirePopoverViewModelTests.swift in Sources */,
+				7B09CBAA2BA4BE8200CF245B /* NetworkProtectionPixelEventTests.swift in Sources */,
 				3706FE7B293F661700E42796 /* HistoryStoringMock.swift in Sources */,
 				562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */,
 				3706FE7C293F661700E42796 /* LocalBookmarkStoreTests.swift in Sources */,
@@ -12509,6 +12514,7 @@
 				FD23FD2B28816606007F6985 /* AutoconsentMessageProtocolTests.swift in Sources */,
 				1D77921A28FDC79800BE0210 /* FaviconStoringMock.swift in Sources */,
 				1D1C36E629FB019C001FA40C /* HistoryTabExtensionTests.swift in Sources */,
+				7B09CBA92BA4BE8100CF245B /* NetworkProtectionPixelEventTests.swift in Sources */,
 				B6DA441E2616C84600DD1EC2 /* PixelStoreMock.swift in Sources */,
 				4B434690285ED7A100177407 /* BookmarksBarViewModelTests.swift in Sources */,
 				B6BBF1702744CDE1004F850E /* CoreDataStoreTests.swift in Sources */,
@@ -13749,7 +13755,7 @@
 			repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit";
 			requirement = {
 				kind = exactVersion;
-				version = 125.0.0;
+				version = 125.0.1;
 			};
 		};
 		AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 24e42fd976..23b4cce189 100644
--- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -32,8 +32,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/duckduckgo/BrowserServicesKit",
       "state" : {
-        "revision" : "ac2127a26f75b2aa293f6036bcdd2bc241d09819",
-        "version" : "125.0.0"
+        "revision" : "eced8f93c945ff2fa4ff92bdd619514d4eff7131",
+        "version" : "125.0.1"
       }
     },
     {
@@ -122,8 +122,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/apple/swift-argument-parser",
       "state" : {
-        "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41",
-        "version" : "1.3.0"
+        "revision" : "46989693916f56d1186bd59ac15124caef896560",
+        "version" : "1.3.1"
       }
     },
     {
diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme
index 58557db074..0224eeaec8 100644
--- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme	
+++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme	
@@ -120,6 +120,9 @@
                <Test
                   Identifier = "PixelStoreTests/testWhenValuesAreRemovedThenTheyAreNotInCache()">
                </Test>
+               <Test
+                  Identifier = "PreferencesSidebarModelTests/testWhenResetTabSelectionIfNeededCalledThenPreferencesTabIsSelected()">
+               </Test>
                <Test
                   Identifier = "StatisticsLoaderTests/testWhenRefreshRetentionAtbIsPerformedForNavigationThenAppRetentionAtbRequested()">
                </Test>
diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift
index f7e491577b..2f60171c17 100644
--- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift
+++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift
@@ -22,22 +22,23 @@ import Foundation
 import PixelKit
 import NetworkProtection
 
-enum NetworkProtectionPixelEvent: PixelKitEvent {
+enum NetworkProtectionPixelEvent: PixelKitEventV2 {
+    static let vpnErrorDomain = "com.duckduckgo.vpn.errorDomain"
 
     case networkProtectionActiveUser
     case networkProtectionNewUser
 
     case networkProtectionControllerStartAttempt
     case networkProtectionControllerStartSuccess
-    case networkProtectionControllerStartFailure
+    case networkProtectionControllerStartFailure(_ error: Error)
 
     case networkProtectionTunnelStartAttempt
     case networkProtectionTunnelStartSuccess
-    case networkProtectionTunnelStartFailure
+    case networkProtectionTunnelStartFailure(_ error: Error)
 
     case networkProtectionTunnelUpdateAttempt
     case networkProtectionTunnelUpdateSuccess
-    case networkProtectionTunnelUpdateFailure
+    case networkProtectionTunnelUpdateFailure(_ error: Error)
 
     case networkProtectionEnableAttemptConnecting
     case networkProtectionEnableAttemptSuccess
@@ -55,23 +56,23 @@ enum NetworkProtectionPixelEvent: PixelKitEvent {
     case networkProtectionTunnelConfigurationCouldNotGetPeerHostName
     case networkProtectionTunnelConfigurationCouldNotGetInterfaceAddressRange
 
-    case networkProtectionClientFailedToFetchServerList(error: Error?)
+    case networkProtectionClientFailedToFetchServerList(_ error: Error?)
     case networkProtectionClientFailedToParseServerListResponse
     case networkProtectionClientFailedToEncodeRegisterKeyRequest
-    case networkProtectionClientFailedToFetchRegisteredServers(error: Error?)
+    case networkProtectionClientFailedToFetchRegisteredServers(_ error: Error?)
     case networkProtectionClientFailedToParseRegisteredServersResponse
     case networkProtectionClientFailedToEncodeRedeemRequest
     case networkProtectionClientInvalidInviteCode
-    case networkProtectionClientFailedToRedeemInviteCode(error: Error?)
-    case networkProtectionClientFailedToParseRedeemResponse(error: Error)
-    case networkProtectionClientFailedToFetchLocations(error: Error?)
-    case networkProtectionClientFailedToParseLocationsResponse(error: Error?)
+    case networkProtectionClientFailedToRedeemInviteCode(_ error: Error?)
+    case networkProtectionClientFailedToParseRedeemResponse(_ error: Error)
+    case networkProtectionClientFailedToFetchLocations(_ error: Error?)
+    case networkProtectionClientFailedToParseLocationsResponse(_ error: Error?)
     case networkProtectionClientInvalidAuthToken
 
     case networkProtectionServerListStoreFailedToEncodeServerList
     case networkProtectionServerListStoreFailedToDecodeServerList
-    case networkProtectionServerListStoreFailedToWriteServerList(error: Error)
-    case networkProtectionServerListStoreFailedToReadServerList(error: Error)
+    case networkProtectionServerListStoreFailedToWriteServerList(_ error: Error)
+    case networkProtectionServerListStoreFailedToReadServerList(_ error: Error)
 
     case networkProtectionKeychainErrorFailedToCastKeychainValueToData(field: String)
     case networkProtectionKeychainReadError(field: String, status: Int32)
@@ -82,14 +83,14 @@ enum NetworkProtectionPixelEvent: PixelKitEvent {
     case networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor
     case networkProtectionWireguardErrorInvalidState(reason: String)
     case networkProtectionWireguardErrorFailedDNSResolution
-    case networkProtectionWireguardErrorCannotSetNetworkSettings(error: Error)
+    case networkProtectionWireguardErrorCannotSetNetworkSettings(_ error: Error)
     case networkProtectionWireguardErrorCannotStartWireguardBackend(code: Int32)
 
     case networkProtectionNoAuthTokenFoundError
 
     case networkProtectionRekeyAttempt
     case networkProtectionRekeyCompleted
-    case networkProtectionRekeyFailure
+    case networkProtectionRekeyFailure(_ error: Error)
 
     case networkProtectionSystemExtensionActivationFailure
 
@@ -102,169 +103,169 @@ enum NetworkProtectionPixelEvent: PixelKitEvent {
         switch self {
 
         case .networkProtectionActiveUser:
-            return "m_mac_netp_daily_active"
+            return "netp_daily_active"
 
         case .networkProtectionNewUser:
-            return "m_mac_netp_daily_active_u"
+            return "netp_daily_active_u"
 
         case .networkProtectionControllerStartAttempt:
-            return "m_mac_netp_controller_start_attempt"
+            return "netp_controller_start_attempt"
 
         case .networkProtectionControllerStartSuccess:
-            return "m_mac_netp_controller_start_success"
+            return "netp_controller_start_success"
 
         case .networkProtectionControllerStartFailure:
-            return "m_mac_netp_controller_start_failure"
+            return "netp_controller_start_failure"
 
         case .networkProtectionTunnelStartAttempt:
-            return "m_mac_netp_tunnel_start_attempt"
+            return "netp_tunnel_start_attempt"
 
         case .networkProtectionTunnelStartSuccess:
-            return "m_mac_netp_tunnel_start_success"
+            return "netp_tunnel_start_success"
 
         case .networkProtectionTunnelStartFailure:
-            return "m_mac_netp_tunnel_start_failure"
+            return "netp_tunnel_start_failure"
 
         case .networkProtectionTunnelUpdateAttempt:
-            return "m_mac_netp_tunnel_update_attempt"
+            return "netp_tunnel_update_attempt"
 
         case .networkProtectionTunnelUpdateSuccess:
-            return "m_mac_netp_tunnel_update_success"
+            return "netp_tunnel_update_success"
 
         case .networkProtectionTunnelUpdateFailure:
-            return "m_mac_netp_tunnel_update_failure"
+            return "netp_tunnel_update_failure"
 
         case .networkProtectionEnableAttemptConnecting:
-            return "m_mac_netp_ev_enable_attempt"
+            return "netp_ev_enable_attempt"
 
         case .networkProtectionEnableAttemptSuccess:
-            return "m_mac_netp_ev_enable_attempt_success"
+            return "netp_ev_enable_attempt_success"
 
         case .networkProtectionEnableAttemptFailure:
-            return "m_mac_netp_ev_enable_attempt_failure"
+            return "netp_ev_enable_attempt_failure"
 
         case .networkProtectionTunnelFailureDetected:
-            return "m_mac_netp_ev_tunnel_failure"
+            return "netp_ev_tunnel_failure"
 
         case .networkProtectionTunnelFailureRecovered:
-            return "m_mac_netp_ev_tunnel_failure_recovered"
+            return "netp_ev_tunnel_failure_recovered"
 
         case .networkProtectionLatency(let quality):
-            return "m_mac_netp_ev_\(quality.rawValue)_latency"
+            return "netp_ev_\(quality.rawValue)_latency"
 
         case .networkProtectionLatencyError:
-            return "m_mac_netp_ev_latency_error"
+            return "netp_ev_latency_error"
 
         case .networkProtectionTunnelConfigurationNoServerRegistrationInfo:
-            return "m_mac_netp_tunnel_config_error_no_server_registration_info"
+            return "netp_tunnel_config_error_no_server_registration_info"
 
         case .networkProtectionTunnelConfigurationCouldNotSelectClosestServer:
-            return "m_mac_netp_tunnel_config_error_could_not_select_closest_server"
+            return "netp_tunnel_config_error_could_not_select_closest_server"
 
         case .networkProtectionTunnelConfigurationCouldNotGetPeerPublicKey:
-            return "m_mac_netp_tunnel_config_error_could_not_get_peer_public_key"
+            return "netp_tunnel_config_error_could_not_get_peer_public_key"
 
         case .networkProtectionTunnelConfigurationCouldNotGetPeerHostName:
-            return "m_mac_netp_tunnel_config_error_could_not_get_peer_host_name"
+            return "netp_tunnel_config_error_could_not_get_peer_host_name"
 
         case .networkProtectionTunnelConfigurationCouldNotGetInterfaceAddressRange:
-            return "m_mac_netp_tunnel_config_error_could_not_get_interface_address_range"
+            return "netp_tunnel_config_error_could_not_get_interface_address_range"
 
         case .networkProtectionClientFailedToFetchServerList:
-            return "m_mac_netp_backend_api_error_failed_to_fetch_server_list"
+            return "netp_backend_api_error_failed_to_fetch_server_list"
 
         case .networkProtectionClientFailedToParseServerListResponse:
-            return "m_mac_netp_backend_api_error_parsing_server_list_response_failed"
+            return "netp_backend_api_error_parsing_server_list_response_failed"
 
         case .networkProtectionClientFailedToEncodeRegisterKeyRequest:
-            return "m_mac_netp_backend_api_error_encoding_register_request_body_failed"
+            return "netp_backend_api_error_encoding_register_request_body_failed"
 
         case .networkProtectionClientFailedToFetchRegisteredServers:
-            return "m_mac_netp_backend_api_error_failed_to_fetch_registered_servers"
+            return "netp_backend_api_error_failed_to_fetch_registered_servers"
 
         case .networkProtectionClientFailedToParseRegisteredServersResponse:
-            return "m_mac_netp_backend_api_error_parsing_device_registration_response_failed"
+            return "netp_backend_api_error_parsing_device_registration_response_failed"
 
         case .networkProtectionClientFailedToEncodeRedeemRequest:
-            return "m_mac_netp_backend_api_error_encoding_redeem_request_body_failed"
+            return "netp_backend_api_error_encoding_redeem_request_body_failed"
 
         case .networkProtectionClientInvalidInviteCode:
-            return "m_mac_netp_backend_api_error_invalid_invite_code"
+            return "netp_backend_api_error_invalid_invite_code"
 
         case .networkProtectionClientFailedToRedeemInviteCode:
-            return "m_mac_netp_backend_api_error_failed_to_redeem_invite_code"
+            return "netp_backend_api_error_failed_to_redeem_invite_code"
 
         case .networkProtectionClientFailedToParseRedeemResponse:
-            return "m_mac_netp_backend_api_error_parsing_redeem_response_failed"
+            return "netp_backend_api_error_parsing_redeem_response_failed"
 
         case .networkProtectionClientFailedToFetchLocations:
-            return "m_mac_netp_backend_api_error_failed_to_fetch_location_list"
+            return "netp_backend_api_error_failed_to_fetch_location_list"
 
         case .networkProtectionClientFailedToParseLocationsResponse:
-            return "m_mac_netp_backend_api_error_parsing_location_list_response_failed"
+            return "netp_backend_api_error_parsing_location_list_response_failed"
 
         case .networkProtectionClientInvalidAuthToken:
-            return "m_mac_netp_backend_api_error_invalid_auth_token"
+            return "netp_backend_api_error_invalid_auth_token"
 
         case .networkProtectionServerListStoreFailedToEncodeServerList:
-            return "m_mac_netp_storage_error_failed_to_encode_server_list"
+            return "netp_storage_error_failed_to_encode_server_list"
 
         case .networkProtectionServerListStoreFailedToDecodeServerList:
-            return "m_mac_netp_storage_error_failed_to_decode_server_list"
+            return "netp_storage_error_failed_to_decode_server_list"
 
         case .networkProtectionServerListStoreFailedToWriteServerList:
-            return "m_mac_netp_storage_error_server_list_file_system_write_failed"
+            return "netp_storage_error_server_list_file_system_write_failed"
 
         case .networkProtectionServerListStoreFailedToReadServerList:
-            return "m_mac_netp_storage_error_server_list_file_system_read_failed"
+            return "netp_storage_error_server_list_file_system_read_failed"
 
         case .networkProtectionKeychainErrorFailedToCastKeychainValueToData:
-            return "m_mac_netp_keychain_error_failed_to_cast_keychain_value_to_data"
+            return "netp_keychain_error_failed_to_cast_keychain_value_to_data"
 
         case .networkProtectionKeychainReadError:
-            return "m_mac_netp_keychain_error_read_failed"
+            return "netp_keychain_error_read_failed"
 
         case .networkProtectionKeychainWriteError:
-            return "m_mac_netp_keychain_error_write_failed"
+            return "netp_keychain_error_write_failed"
 
         case .networkProtectionKeychainUpdateError:
-            return "m_mac_netp_keychain_error_update_failed"
+            return "netp_keychain_error_update_failed"
 
         case .networkProtectionKeychainDeleteError:
-            return "m_mac_netp_keychain_error_delete_failed"
+            return "netp_keychain_error_delete_failed"
 
         case .networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor:
-            return "m_mac_netp_wireguard_error_cannot_locate_tunnel_file_descriptor"
+            return "netp_wireguard_error_cannot_locate_tunnel_file_descriptor"
 
         case .networkProtectionWireguardErrorInvalidState:
-            return "m_mac_netp_wireguard_error_invalid_state"
+            return "netp_wireguard_error_invalid_state"
 
         case .networkProtectionWireguardErrorFailedDNSResolution:
-            return "m_mac_netp_wireguard_error_failed_dns_resolution"
+            return "netp_wireguard_error_failed_dns_resolution"
 
         case .networkProtectionWireguardErrorCannotSetNetworkSettings:
-            return "m_mac_netp_wireguard_error_cannot_set_network_settings"
+            return "netp_wireguard_error_cannot_set_network_settings"
 
         case .networkProtectionWireguardErrorCannotStartWireguardBackend:
-            return "m_mac_netp_wireguard_error_cannot_start_wireguard_backend"
+            return "netp_wireguard_error_cannot_start_wireguard_backend"
 
         case .networkProtectionNoAuthTokenFoundError:
-            return "m_mac_netp_no_auth_token_found_error"
+            return "netp_no_auth_token_found_error"
 
         case .networkProtectionRekeyAttempt:
-            return "m_mac_netp_rekey_attempt"
+            return "netp_rekey_attempt"
 
         case .networkProtectionRekeyCompleted:
-            return "m_mac_netp_rekey_completed"
+            return "netp_rekey_completed"
 
         case .networkProtectionRekeyFailure:
-            return "m_mac_netp_rekey_failure"
+            return "netp_rekey_failure"
 
         case .networkProtectionSystemExtensionActivationFailure:
-            return "m_mac_netp_system_extension_activation_failure"
+            return "netp_system_extension_activation_failure"
 
         case .networkProtectionUnhandledError:
-            return "m_mac_netp_unhandled_error"
+            return "netp_unhandled_error"
         }
     }
 
@@ -308,13 +309,13 @@ enum NetworkProtectionPixelEvent: PixelKitEvent {
         case .networkProtectionClientFailedToFetchRegisteredServers(let error):
             return error?.pixelParameters
 
-        case .networkProtectionClientFailedToRedeemInviteCode(error: let error):
+        case .networkProtectionClientFailedToRedeemInviteCode(let error):
             return error?.pixelParameters
 
-        case .networkProtectionClientFailedToFetchLocations(error: let error):
+        case .networkProtectionClientFailedToFetchLocations(let error):
             return error?.pixelParameters
 
-        case .networkProtectionClientFailedToParseLocationsResponse(error: let error):
+        case .networkProtectionClientFailedToParseLocationsResponse(let error):
             return error?.pixelParameters
 
         case .networkProtectionUnhandledError(let function, let line, let error):
@@ -323,7 +324,7 @@ enum NetworkProtectionPixelEvent: PixelKitEvent {
             parameters[PixelKit.Parameters.line] = String(line)
             return parameters
 
-        case .networkProtectionWireguardErrorCannotSetNetworkSettings(error: let error):
+        case .networkProtectionWireguardErrorCannotSetNetworkSettings(let error):
             return error.pixelParameters
 
         case .networkProtectionWireguardErrorCannotStartWireguardBackend(code: let code):
@@ -379,6 +380,69 @@ enum NetworkProtectionPixelEvent: PixelKitEvent {
             return nil
         }
     }
+
+    var error: (any Error)? {
+        switch self {
+        case .networkProtectionActiveUser,
+                .networkProtectionNewUser,
+                .networkProtectionControllerStartAttempt,
+                .networkProtectionControllerStartSuccess,
+                .networkProtectionTunnelStartAttempt,
+                .networkProtectionTunnelStartSuccess,
+                .networkProtectionTunnelUpdateAttempt,
+                .networkProtectionTunnelUpdateSuccess,
+                .networkProtectionEnableAttemptConnecting,
+                .networkProtectionEnableAttemptSuccess,
+                .networkProtectionEnableAttemptFailure,
+                .networkProtectionTunnelFailureDetected,
+                .networkProtectionTunnelFailureRecovered,
+                .networkProtectionLatencyError,
+                .networkProtectionLatency,
+                .networkProtectionTunnelConfigurationNoServerRegistrationInfo,
+                .networkProtectionTunnelConfigurationCouldNotGetPeerPublicKey,
+                .networkProtectionTunnelConfigurationCouldNotGetPeerHostName,
+                .networkProtectionTunnelConfigurationCouldNotGetInterfaceAddressRange,
+                .networkProtectionClientFailedToParseServerListResponse,
+                .networkProtectionClientFailedToEncodeRegisterKeyRequest,
+                .networkProtectionClientFailedToParseRegisteredServersResponse,
+                .networkProtectionTunnelConfigurationCouldNotSelectClosestServer,
+                .networkProtectionClientFailedToEncodeRedeemRequest,
+                .networkProtectionClientInvalidInviteCode,
+                .networkProtectionClientInvalidAuthToken,
+                .networkProtectionServerListStoreFailedToEncodeServerList,
+                .networkProtectionServerListStoreFailedToDecodeServerList,
+                .networkProtectionKeychainErrorFailedToCastKeychainValueToData,
+                .networkProtectionKeychainReadError,
+                .networkProtectionKeychainWriteError,
+                .networkProtectionKeychainUpdateError,
+                .networkProtectionKeychainDeleteError,
+                .networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor,
+                .networkProtectionWireguardErrorInvalidState,
+                .networkProtectionWireguardErrorFailedDNSResolution,
+                .networkProtectionWireguardErrorCannotStartWireguardBackend,
+                .networkProtectionNoAuthTokenFoundError,
+                .networkProtectionRekeyAttempt,
+                .networkProtectionRekeyCompleted,
+                .networkProtectionSystemExtensionActivationFailure:
+            return nil
+        case .networkProtectionClientFailedToRedeemInviteCode(let error),
+                .networkProtectionClientFailedToFetchLocations(let error),
+                .networkProtectionClientFailedToParseLocationsResponse(let error),
+                .networkProtectionClientFailedToFetchServerList(let error),
+                .networkProtectionClientFailedToFetchRegisteredServers(let error):
+            return error
+        case .networkProtectionControllerStartFailure(let error),
+                .networkProtectionTunnelStartFailure(let error),
+                .networkProtectionTunnelUpdateFailure(let error),
+                .networkProtectionClientFailedToParseRedeemResponse(let error),
+                .networkProtectionServerListStoreFailedToWriteServerList(let error),
+                .networkProtectionServerListStoreFailedToReadServerList(let error),
+                .networkProtectionWireguardErrorCannotSetNetworkSettings(let error),
+                .networkProtectionRekeyFailure(let error),
+                .networkProtectionUnhandledError(_, _, let error):
+            return error
+        }
+    }
 }
 
 #endif
diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift
index b363c3d843..3a42455d0f 100644
--- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift
+++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift
@@ -36,10 +36,10 @@ extension EventMapping where Event == NetworkProtectionError {
             domainEvent = .networkProtectionClientInvalidInviteCode
             frequency = .standard
         case .failedToRedeemInviteCode(let error):
-            domainEvent = .networkProtectionClientFailedToRedeemInviteCode(error: error)
+            domainEvent = .networkProtectionClientFailedToRedeemInviteCode(error)
             frequency = .standard
         case .failedToParseRedeemResponse(let error):
-            domainEvent = .networkProtectionClientFailedToParseRedeemResponse(error: error)
+            domainEvent = .networkProtectionClientFailedToParseRedeemResponse(error)
             frequency = .standard
         case .invalidAuthToken:
             domainEvent = .networkProtectionClientInvalidAuthToken
@@ -63,10 +63,10 @@ extension EventMapping where Event == NetworkProtectionError {
             domainEvent = .networkProtectionNoAuthTokenFoundError
             frequency = .standard
         case .failedToFetchLocationList(let error):
-            domainEvent = .networkProtectionClientFailedToFetchLocations(error: error)
+            domainEvent = .networkProtectionClientFailedToFetchLocations(error)
             frequency = .dailyAndContinuous
         case .failedToParseLocationListResponse(let error):
-            domainEvent = .networkProtectionClientFailedToParseLocationsResponse(error: error)
+            domainEvent = .networkProtectionClientFailedToParseLocationsResponse(error)
             frequency = .dailyAndContinuous
         case .noServerRegistrationInfo,
                 .couldNotSelectClosestServer,
diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift
index 0ea6d48740..4132457ca2 100644
--- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift
+++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift
@@ -519,7 +519,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr
             }
         } catch {
             PixelKit.fire(
-                NetworkProtectionPixelEvent.networkProtectionControllerStartFailure, frequency: .dailyAndContinuous, withError: error, includeAppVersionParameter: true
+                NetworkProtectionPixelEvent.networkProtectionControllerStartFailure(error), frequency: .dailyAndContinuous, includeAppVersionParameter: true
             )
 
             await stop()
diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift
index d7d6de4627..da8d81fc24 100644
--- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift
+++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift
@@ -59,13 +59,13 @@ final class MacPacketTunnelProvider: PacketTunnelProvider {
             case .couldNotGetInterfaceAddressRange:
                 domainEvent = .networkProtectionTunnelConfigurationCouldNotGetInterfaceAddressRange
             case .failedToFetchServerList(let eventError):
-                domainEvent = .networkProtectionClientFailedToFetchServerList(error: eventError)
+                domainEvent = .networkProtectionClientFailedToFetchServerList(eventError)
             case .failedToParseServerListResponse:
                 domainEvent = .networkProtectionClientFailedToParseServerListResponse
             case .failedToEncodeRegisterKeyRequest:
                 domainEvent = .networkProtectionClientFailedToEncodeRegisterKeyRequest
             case .failedToFetchRegisteredServers(let eventError):
-                domainEvent = .networkProtectionClientFailedToFetchRegisteredServers(error: eventError)
+                domainEvent = .networkProtectionClientFailedToFetchRegisteredServers(eventError)
             case .failedToParseRegisteredServersResponse:
                 domainEvent = .networkProtectionClientFailedToParseRegisteredServersResponse
             case .failedToEncodeRedeemRequest:
@@ -73,9 +73,9 @@ final class MacPacketTunnelProvider: PacketTunnelProvider {
             case .invalidInviteCode:
                 domainEvent = .networkProtectionClientInvalidInviteCode
             case .failedToRedeemInviteCode(let error):
-                domainEvent = .networkProtectionClientFailedToRedeemInviteCode(error: error)
+                domainEvent = .networkProtectionClientFailedToRedeemInviteCode(error)
             case .failedToParseRedeemResponse(let error):
-                domainEvent = .networkProtectionClientFailedToParseRedeemResponse(error: error)
+                domainEvent = .networkProtectionClientFailedToParseRedeemResponse(error)
             case .invalidAuthToken:
                 domainEvent = .networkProtectionClientInvalidAuthToken
             case .serverListInconsistency:
@@ -85,13 +85,13 @@ final class MacPacketTunnelProvider: PacketTunnelProvider {
             case .failedToDecodeServerList:
                 domainEvent = .networkProtectionServerListStoreFailedToDecodeServerList
             case .failedToWriteServerList(let eventError):
-                domainEvent = .networkProtectionServerListStoreFailedToWriteServerList(error: eventError)
+                domainEvent = .networkProtectionServerListStoreFailedToWriteServerList(eventError)
             case .noServerListFound:
                 return
             case .couldNotCreateServerListDirectory:
                 return
             case .failedToReadServerList(let eventError):
-                domainEvent = .networkProtectionServerListStoreFailedToReadServerList(error: eventError)
+                domainEvent = .networkProtectionServerListStoreFailedToReadServerList(eventError)
             case .failedToCastKeychainValueToData(let field):
                 domainEvent = .networkProtectionKeychainErrorFailedToCastKeychainValueToData(field: field)
             case .keychainReadError(let field, let status):
@@ -109,7 +109,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider {
             case .wireGuardDnsResolution:
                 domainEvent = .networkProtectionWireguardErrorFailedDNSResolution
             case .wireGuardSetNetworkSettings(let error):
-                domainEvent = .networkProtectionWireguardErrorCannotSetNetworkSettings(error: error)
+                domainEvent = .networkProtectionWireguardErrorCannotSetNetworkSettings(error)
             case .startWireGuardBackend(let code):
                 domainEvent = .networkProtectionWireguardErrorCannotStartWireguardBackend(code: code)
             case .noAuthTokenFound:
@@ -206,9 +206,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider {
                     includeAppVersionParameter: true)
             case .failure(let error):
                 PixelKit.fire(
-                    NetworkProtectionPixelEvent.networkProtectionRekeyFailure,
+                    NetworkProtectionPixelEvent.networkProtectionRekeyFailure(error),
                     frequency: .dailyAndContinuous,
-                    withError: error,
                     includeAppVersionParameter: true)
             case .success:
                 PixelKit.fire(
@@ -225,9 +224,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider {
                     includeAppVersionParameter: true)
             case .failure(let error):
                 PixelKit.fire(
-                    NetworkProtectionPixelEvent.networkProtectionTunnelStartFailure,
+                    NetworkProtectionPixelEvent.networkProtectionTunnelStartFailure(error),
                     frequency: .dailyAndContinuous,
-                    withError: error,
                     includeAppVersionParameter: true)
             case .success:
                 PixelKit.fire(
@@ -244,9 +242,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider {
                     includeAppVersionParameter: true)
             case .failure(let error):
                 PixelKit.fire(
-                    NetworkProtectionPixelEvent.networkProtectionTunnelUpdateFailure,
+                    NetworkProtectionPixelEvent.networkProtectionTunnelUpdateFailure(error),
                     frequency: .dailyAndContinuous,
-                    withError: error,
                     includeAppVersionParameter: true)
             case .success:
                 PixelKit.fire(
diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift
index d428da60b2..20e8b1f4b2 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: "125.0.0"),
+        .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.1"),
         .package(path: "../PixelKit"),
         .package(path: "../SwiftUIExtensions"),
         .package(path: "../XPCHelper"),
diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift
index 39f13d9fe0..baddd89369 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: "125.0.0"),
+        .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.1"),
         .package(path: "../XPCHelper"),
         .package(path: "../SwiftUIExtensions"),
         .package(path: "../LoginItems"),
diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift
index ea08352d29..e368d38db5 100644
--- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift
+++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift
@@ -20,7 +20,7 @@ import Foundation
 
 public extension PixelKit {
 
-    enum Parameters {
+    enum Parameters: Hashable {
         public static let duration = "duration"
         public static let test = "test"
         public static let appVersion = "appVersion"
diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift
index 067eee091e..db56d6664b 100644
--- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift
+++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift
@@ -17,6 +17,7 @@
 //
 
 import Foundation
+import PixelKit
 
 /// Structure containing information about a pixel fire event.
 ///
@@ -27,10 +28,35 @@ public struct PixelFireExpectations {
     let pixelName: String
     var error: Error?
     var underlyingError: Error?
+    var customFields: [String: String]?
 
-    public init(pixelName: String, error: Error? = nil, underlyingError: Error? = nil) {
+    /// Convenience initializer for cleaner semantics
+    ///
+    public static func expect(pixelName: String, error: Error? = nil, underlyingError: Error? = nil, customFields: [String: String]? = nil) -> PixelFireExpectations {
+
+        .init(pixelName: pixelName, error: error, underlyingError: underlyingError, customFields: customFields)
+    }
+
+    public init(pixelName: String, error: Error? = nil, underlyingError: Error? = nil, customFields: [String: String]? = nil) {
         self.pixelName = pixelName
         self.error = error
         self.underlyingError = underlyingError
+        self.customFields = customFields
+    }
+
+    public var parameters: [String: String] {
+        var parameters = customFields ?? [String: String]()
+
+        if let nsError = error as? NSError {
+            parameters[PixelKit.Parameters.errorCode] = String(nsError.code)
+            parameters[PixelKit.Parameters.errorDomain] = nsError.domain
+        }
+
+        if let nsUnderlyingError = underlyingError as? NSError {
+            parameters[PixelKit.Parameters.underlyingErrorCode] = String(nsUnderlyingError.code)
+            parameters[PixelKit.Parameters.underlyingErrorDomain] = nsUnderlyingError.domain
+        }
+
+        return parameters
     }
 }
diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift
index 325b7414b3..6d742bd532 100644
--- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift
+++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift
@@ -66,7 +66,13 @@ public extension XCTestCase {
 #endif
     }
 
-    func expectedParameters(for event: PixelKitEventV2) -> [String: String] {
+    /// These parameters are known to be expected just based on the event definition.
+    ///
+    /// They're not a complete list of parameters for the event, as the fire call may contain extra information
+    /// that results in additional parameters.  Ideally we want most (if not all) that information to eventually
+    /// make part of the pixel definition.
+    ///
+    func knownExpectedParameters(for event: PixelKitEventV2) -> [String: String] {
         var expectedParameters = [String: String]()
 
         if let error = event.error {
@@ -74,10 +80,9 @@ public extension XCTestCase {
             expectedParameters[PixelKit.Parameters.errorCode] = "\(nsError.code)"
             expectedParameters[PixelKit.Parameters.errorDomain] = nsError.domain
 
-            if let underlyingError = (error as? PixelKitEventErrorDetails)?.underlyingError {
-                let underlyingNSError = underlyingError as NSError
-                expectedParameters[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)"
-                expectedParameters[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain
+            if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError {
+                expectedParameters[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)"
+                expectedParameters[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain
             }
         }
 
@@ -92,14 +97,18 @@ public extension XCTestCase {
 
     // MARK: - Pixel Firing Expectations
 
+    func fire(_ event: PixelKitEventV2, and expectations: PixelFireExpectations, file: StaticString, line: UInt) {
+        verifyThat(event, meets: expectations, file: file, line: line)
+    }
+
     /// 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 expectedPixelName = event.name.hasPrefix(Self.pixelPlatformPrefix) ? event.name : Self.pixelPlatformPrefix + event.name
+        let knownExpectedParameters = knownExpectedParameters(for: event)
         let callbackExecutedExpectation = expectation(description: "The PixelKit callback has been executed")
 
         PixelKit.setUp(dryRun: false,
@@ -115,23 +124,14 @@ public extension XCTestCase {
             // Internal validations
 
             XCTAssertEqual(firedPixelName, expectedPixelName, file: file, line: line)
-            XCTAssertEqual(firedParameters, expectedParameters, file: file, line: line)
 
-            // Expectations
+            XCTAssertTrue(knownExpectedParameters.allSatisfy { (key, value) in
+                firedParameters[key] == value
+            })
 
+            // 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)
-            }
-
-            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, expectations.parameters)
 
             completion(true, nil)
         }
diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift
index 6ffb49191b..ef458f05c7 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: "125.0.0"),
+        .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "125.0.1"),
         .package(path: "../SwiftUIExtensions")
     ],
     targets: [
diff --git a/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift b/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift
new file mode 100644
index 0000000000..01a2cc72b1
--- /dev/null
+++ b/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift
@@ -0,0 +1,345 @@
+//
+//  NetworkProtectionPixelEventTests.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.
+//
+
+#if NETWORK_PROTECTION
+
+import NetworkProtection
+import PixelKit
+import PixelKitTestingUtilities
+import XCTest
+@testable import DuckDuckGo_Privacy_Browser
+
+final class NetworkProtectionPixelEventTests: XCTestCase {
+
+    private enum TestError: CustomNSError {
+        case testError
+        case underlyingError
+
+        /// The domain of the error.
+        static var errorDomain: String {
+            "testDomain"
+        }
+
+        /// The error code within the given domain.
+        var errorCode: Int {
+            switch self {
+            case .testError: return 1
+            case .underlyingError: return 2
+            }
+        }
+
+        /// The user-info dictionary.
+        var errorUserInfo: [String: Any] {
+            switch self {
+            case .testError:
+                return [NSUnderlyingErrorKey: TestError.underlyingError]
+            case .underlyingError:
+                return [:]
+            }
+        }
+    }
+
+    // MARK: - Test Firing Pixels
+
+    /// This test verifies validates expectations when firing `NetworkProtectionPixelEvent`.
+    ///
+    /// This test verifies a few different things:
+    ///  - That the pixel name is not changed by mistake.
+    ///  - That when the pixel is fired its name and parameters are exactly what's expected.
+    ///
+    func testVPNPixelFireExpectations() {
+        fire(NetworkProtectionPixelEvent.networkProtectionActiveUser,
+             and: .expect(pixelName: "m_mac_netp_daily_active"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionNewUser,
+             and: .expect(pixelName: "m_mac_netp_daily_active_u"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionControllerStartAttempt,
+             and: .expect(pixelName: "m_mac_netp_controller_start_attempt"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionControllerStartFailure(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_controller_start_failure",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionControllerStartSuccess,
+             and: .expect(pixelName: "m_mac_netp_controller_start_success"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelStartAttempt,
+             and: .expect(pixelName: "m_mac_netp_tunnel_start_attempt"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelStartFailure(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_tunnel_start_failure",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelStartSuccess,
+             and: .expect(pixelName: "m_mac_netp_tunnel_start_success"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelUpdateAttempt,
+             and: .expect(pixelName: "m_mac_netp_tunnel_update_attempt"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelUpdateFailure(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_tunnel_update_failure",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelUpdateSuccess,
+             and: .expect(pixelName: "m_mac_netp_tunnel_update_success"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionEnableAttemptConnecting,
+             and: .expect(pixelName: "m_mac_netp_ev_enable_attempt"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionEnableAttemptSuccess,
+             and: .expect(pixelName: "m_mac_netp_ev_enable_attempt_success"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionEnableAttemptFailure,
+             and: .expect(pixelName: "m_mac_netp_ev_enable_attempt_failure"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelFailureDetected,
+             and: .expect(pixelName: "m_mac_netp_ev_tunnel_failure"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelFailureRecovered,
+             and: .expect(pixelName: "m_mac_netp_ev_tunnel_failure_recovered"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionLatency(quality: .excellent),
+             and: .expect(pixelName: "m_mac_netp_ev_\(NetworkProtectionLatencyMonitor.ConnectionQuality.excellent.rawValue)_latency"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionLatencyError,
+             and: .expect(pixelName: "m_mac_netp_ev_latency_error"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelConfigurationNoServerRegistrationInfo,
+             and: .expect(pixelName: "m_mac_netp_tunnel_config_error_no_server_registration_info"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelConfigurationCouldNotSelectClosestServer,
+             and: .expect(pixelName: "m_mac_netp_tunnel_config_error_could_not_select_closest_server"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelConfigurationCouldNotGetPeerPublicKey,
+             and: .expect(pixelName: "m_mac_netp_tunnel_config_error_could_not_get_peer_public_key"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelConfigurationCouldNotGetPeerHostName,
+             and: .expect(pixelName: "m_mac_netp_tunnel_config_error_could_not_get_peer_host_name"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionTunnelConfigurationCouldNotGetInterfaceAddressRange,
+             and: .expect(pixelName: "m_mac_netp_tunnel_config_error_could_not_get_interface_address_range"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToFetchServerList(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_backend_api_error_failed_to_fetch_server_list",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToParseServerListResponse,
+             and: .expect(pixelName: "m_mac_netp_backend_api_error_parsing_server_list_response_failed"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToEncodeRegisterKeyRequest,
+             and: .expect(pixelName: "m_mac_netp_backend_api_error_encoding_register_request_body_failed"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToFetchRegisteredServers(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_backend_api_error_failed_to_fetch_registered_servers",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToParseRegisteredServersResponse,
+             and: .expect(pixelName: "m_mac_netp_backend_api_error_parsing_device_registration_response_failed"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToEncodeRedeemRequest,
+             and: .expect(pixelName: "m_mac_netp_backend_api_error_encoding_redeem_request_body_failed"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionClientInvalidInviteCode,
+             and: .expect(pixelName: "m_mac_netp_backend_api_error_invalid_invite_code"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToRedeemInviteCode(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_backend_api_error_failed_to_redeem_invite_code",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToParseRedeemResponse(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_backend_api_error_parsing_redeem_response_failed",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToFetchLocations(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_backend_api_error_failed_to_fetch_location_list",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToParseLocationsResponse(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_backend_api_error_parsing_location_list_response_failed",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionClientInvalidAuthToken,
+             and: .expect(pixelName: "m_mac_netp_backend_api_error_invalid_auth_token"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionServerListStoreFailedToEncodeServerList,
+             and: .expect(pixelName: "m_mac_netp_storage_error_failed_to_encode_server_list"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionServerListStoreFailedToDecodeServerList,
+             and: .expect(pixelName: "m_mac_netp_storage_error_failed_to_decode_server_list"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionServerListStoreFailedToWriteServerList(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_storage_error_server_list_file_system_write_failed",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionServerListStoreFailedToReadServerList(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_storage_error_server_list_file_system_read_failed",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionKeychainErrorFailedToCastKeychainValueToData(field: "field"),
+             and: .expect(pixelName: "m_mac_netp_keychain_error_failed_to_cast_keychain_value_to_data",
+                          customFields: [
+                            PixelKit.Parameters.keychainFieldName: "field",
+                          ]),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionKeychainReadError(field: "field", status: 1),
+             and: .expect(pixelName: "m_mac_netp_keychain_error_read_failed",
+                          customFields: [
+                            PixelKit.Parameters.keychainFieldName: "field",
+                            PixelKit.Parameters.errorCode: "1",
+                          ]),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionKeychainWriteError(field: "field", status: 1),
+             and: .expect(pixelName: "m_mac_netp_keychain_error_write_failed",
+                          customFields: [
+                            PixelKit.Parameters.keychainFieldName: "field",
+                            PixelKit.Parameters.errorCode: "1",
+                          ]),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionKeychainUpdateError(field: "field", status: 1),
+             and: .expect(pixelName: "m_mac_netp_keychain_error_update_failed",
+                          customFields: [
+                            PixelKit.Parameters.keychainFieldName: "field",
+                            PixelKit.Parameters.errorCode: "1",
+                          ]),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionKeychainDeleteError(status: 1),
+             and: .expect(pixelName: "m_mac_netp_keychain_error_delete_failed",
+                          customFields: [
+                            PixelKit.Parameters.errorCode: "1"
+                          ]),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor,
+             and: .expect(pixelName: "m_mac_netp_wireguard_error_cannot_locate_tunnel_file_descriptor"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionWireguardErrorInvalidState(reason: "reason"),
+             and: .expect(pixelName: "m_mac_netp_wireguard_error_invalid_state",
+                          customFields: [
+                            PixelKit.Parameters.reason: "reason"
+                          ]),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionWireguardErrorFailedDNSResolution,
+             and: .expect(pixelName: "m_mac_netp_wireguard_error_failed_dns_resolution"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionWireguardErrorCannotSetNetworkSettings(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_wireguard_error_cannot_set_network_settings",
+                                 error: TestError.testError,
+                                 underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionWireguardErrorCannotStartWireguardBackend(code: 1),
+             and: .expect(pixelName: "m_mac_netp_wireguard_error_cannot_start_wireguard_backend",
+                          customFields: [
+                            PixelKit.Parameters.errorCode: "1"
+                          ]),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionNoAuthTokenFoundError,
+             and: .expect(pixelName: "m_mac_netp_no_auth_token_found_error"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionRekeyAttempt,
+             and: .expect(pixelName: "m_mac_netp_rekey_attempt"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionRekeyCompleted,
+             and: .expect(pixelName: "m_mac_netp_rekey_completed"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionRekeyFailure(TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_rekey_failure",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure,
+             and: .expect(pixelName: "m_mac_netp_system_extension_activation_failure"),
+             file: #filePath,
+             line: #line)
+        fire(NetworkProtectionPixelEvent.networkProtectionUnhandledError(function: "function", line: 1, error: TestError.testError),
+             and: .expect(pixelName: "m_mac_netp_unhandled_error",
+                          error: TestError.testError,
+                          underlyingError: TestError.underlyingError,
+                          customFields: [
+                            PixelKit.Parameters.function: "function",
+                            PixelKit.Parameters.line: "1",
+                          ]),
+             file: #filePath,
+             line: #line)
+    }
+}
+
+#endif