From aabc6a5b49b8b12d8b89036eff3ed75ec1086ae3 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Thu, 28 Nov 2024 19:12:08 -0800 Subject: [PATCH 01/12] [Release PR] Add StoreKit debugging metadata to the debug menu (#3637) Task/Issue URL: https://app.asana.com/0/1199333091098016/1208862631410451/f Tech Design URL: CC: Description: This PR adds StoreKit metadata to the debug menu. --- DuckDuckGo/Debug.storyboard | 35 ++++++++++++---- .../SubscriptionDebugViewController.swift | 41 ++++++++++++++++++- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo/Debug.storyboard b/DuckDuckGo/Debug.storyboard index 4f26190100..b09aefbf25 100644 --- a/DuckDuckGo/Debug.storyboard +++ b/DuckDuckGo/Debug.storyboard @@ -1001,12 +1001,28 @@ - + + + + + @@ -1031,17 +1047,17 @@ - + - + - + - + @@ -1125,6 +1141,9 @@ + + + diff --git a/DuckDuckGo/SubscriptionDebugViewController.swift b/DuckDuckGo/SubscriptionDebugViewController.swift index 45247ddb76..5bd7505d64 100644 --- a/DuckDuckGo/SubscriptionDebugViewController.swift +++ b/DuckDuckGo/SubscriptionDebugViewController.swift @@ -22,6 +22,7 @@ import UIKit import Subscription import Core import NetworkProtection +import StoreKit final class SubscriptionDebugViewController: UITableViewController { @@ -39,6 +40,7 @@ final class SubscriptionDebugViewController: UITableViewController { Sections.appstore: "App Store", Sections.environment: "Environment", Sections.pixels: "Promo Pixel Parameters", + Sections.metadata: "StoreKit Metadata" ] enum Sections: Int, CaseIterable { @@ -47,6 +49,7 @@ final class SubscriptionDebugViewController: UITableViewController { case appstore case environment case pixels + case metadata } enum AuthorizationRows: Int, CaseIterable { @@ -74,10 +77,23 @@ final class SubscriptionDebugViewController: UITableViewController { case randomize } + enum MetadataRows: Int, CaseIterable { + case storefrontID + case countryCode + } + + private var storefrontID = "Loading" + private var storefrontCountryCode = "Loading" + override func numberOfSections(in tableView: UITableView) -> Int { return Sections.allCases.count } + override func viewDidLoad() { + super.viewDidLoad() + loadStoreKitMetadata() + } + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { guard let section = Sections(rawValue: section) else { return nil } return titles[section] @@ -145,6 +161,18 @@ final class SubscriptionDebugViewController: UITableViewController { case .none: break } + + case .metadata: + switch MetadataRows(rawValue: indexPath.row) { + case .storefrontID: + cell.textLabel?.text = "Storefront ID" + cell.detailTextLabel?.text = storefrontID + case .countryCode: + cell.textLabel?.text = "Country Code" + cell.detailTextLabel?.text = storefrontCountryCode + case .none: + break + } case .none: break } @@ -159,8 +187,8 @@ final class SubscriptionDebugViewController: UITableViewController { case .appstore: return AppStoreRows.allCases.count case .environment: return EnvironmentRows.allCases.count case .pixels: return PixelsRows.allCases.count + case .metadata: return MetadataRows.allCases.count case .none: return 0 - } } @@ -193,6 +221,8 @@ final class SubscriptionDebugViewController: UITableViewController { case .randomize: showRandomizedParamters() default: break } + case .metadata: + break case .none: break } @@ -389,6 +419,15 @@ final class SubscriptionDebugViewController: UITableViewController { NetworkProtectionLocationListCompositeRepository.clearCache() } } + + private func loadStoreKitMetadata() { + Task { @MainActor in + let storefront = await Storefront.current + self.storefrontID = storefront?.id ?? "nil" + self.storefrontCountryCode = storefront?.countryCode ?? "nil" + self.tableView.reloadData() + } + } } extension Bool { From 286e5b99ac7c37d96bee3fc64167fb62a6e64022 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Thu, 28 Nov 2024 20:20:14 -0800 Subject: [PATCH 02/12] Release 7.147.0-5 (#3649) Please make sure all GH checks passed before merging. It can take around 20 minutes. Briefly review this PR to see if there are no issues or red flags and then merge it. --- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f5ecd0d022..3800ace64b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9271,7 +9271,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9308,7 +9308,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9398,7 +9398,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9425,7 +9425,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9572,7 +9572,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9597,7 +9597,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9666,7 +9666,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9700,7 +9700,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9733,7 +9733,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9763,7 +9763,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10073,7 +10073,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10104,7 +10104,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10132,7 +10132,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10165,7 +10165,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10195,7 +10195,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10228,11 +10228,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 4; + DYLIB_CURRENT_VERSION = 5; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10464,7 +10464,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10492,7 +10492,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10524,7 +10524,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10561,7 +10561,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10596,7 +10596,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10631,11 +10631,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 4; + DYLIB_CURRENT_VERSION = 5; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10808,11 +10808,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 4; + DYLIB_CURRENT_VERSION = 5; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10841,10 +10841,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 4; + DYLIB_CURRENT_VERSION = 5; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; From cc448e015b7fb8e7ce3f5f2e9a20cac9cf18dff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Fri, 29 Nov 2024 10:31:38 +0100 Subject: [PATCH 03/12] Include inactive tabs statistics in daily tab switcher pixel (#3643) Task/Issue URL: https://app.asana.com/0/72649045549333/1208778471392233/f Tech Design URL: CC: **Description**: Adds statistics information about inactive tabs to `m_tab_manager_clicked_daily` pixel. Although Tab model has been changed, migration is not necessary. New property is optional and is defaulting to `nil` in case no value has been found. Pixel details can be found in [this task](https://app.asana.com/0/0/1208778471392225/f). **Steps to test this PR**: Prerequisite: Change `DailyPixel.hasBeenFiredToday` to return `false`. 1. Build and run current public version (can be any recent one) or make sure you already have a few tabs opened. 2. Build and run this code. 3. Observe pixel after opening tab switcher: 1. There should be 0 inactive tabs reported (as they don't have last activity date stored) 2. Go to a few of opened tabs, on next pixel fire they should be counted as inactive tabs in respective bucket (`tab_active_7d`). 4. Open a bunch of new tabs. 5. Observe pixel parameters include new tabs in counts after opening tab switcher. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- DuckDuckGo.xcodeproj/project.pbxproj | 8 +- DuckDuckGo/Tab.swift | 21 +- DuckDuckGo/TabSwitcherOpenDailyPixel.swift | 65 ++++- .../TabSwitcherDailyPixelTests.swift | 99 ------- .../TabSwitcherOpenDailyPixelTests.swift | 257 ++++++++++++++++++ 5 files changed, 343 insertions(+), 107 deletions(-) delete mode 100644 DuckDuckGoTests/TabSwitcherDailyPixelTests.swift create mode 100644 DuckDuckGoTests/TabSwitcherOpenDailyPixelTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 69e79ff8df..3273c57da7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -366,7 +366,7 @@ 6FEC0B882C999961006B4F6E /* FavoritesListInteractingAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */; }; 6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */; }; 6FF9AD3F2CE63DD800C5A406 /* TabSwitcherOpenDailyPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD3E2CE63DC200C5A406 /* TabSwitcherOpenDailyPixel.swift */; }; - 6FF9AD412CE6610F00C5A406 /* TabSwitcherDailyPixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD402CE6610600C5A406 /* TabSwitcherDailyPixelTests.swift */; }; + 6FF9AD412CE6610F00C5A406 /* TabSwitcherOpenDailyPixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD402CE6610600C5A406 /* TabSwitcherOpenDailyPixelTests.swift */; }; 6FF9AD452CE766F700C5A406 /* NewTabPageControllerPixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD442CE766F700C5A406 /* NewTabPageControllerPixelTests.swift */; }; 7B1604E82CB685B400A44EC6 /* Logger+TipKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */; }; 7B1604EC2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */; }; @@ -1699,7 +1699,7 @@ 6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesListInteractingAdapter.swift; sourceTree = ""; }; 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionFetcherTests.swift; sourceTree = ""; }; 6FF9AD3E2CE63DC200C5A406 /* TabSwitcherOpenDailyPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSwitcherOpenDailyPixel.swift; sourceTree = ""; }; - 6FF9AD402CE6610600C5A406 /* TabSwitcherDailyPixelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSwitcherDailyPixelTests.swift; sourceTree = ""; }; + 6FF9AD402CE6610600C5A406 /* TabSwitcherOpenDailyPixelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSwitcherOpenDailyPixelTests.swift; sourceTree = ""; }; 6FF9AD442CE766F700C5A406 /* NewTabPageControllerPixelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerPixelTests.swift; sourceTree = ""; }; 7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+TipKit.swift"; sourceTree = ""; }; 7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TipKitController+ConvenienceInitializers.swift"; sourceTree = ""; }; @@ -5992,7 +5992,7 @@ F13B4BF71F18C9E800814661 /* Tabs */ = { isa = PBXGroup; children = ( - 6FF9AD402CE6610600C5A406 /* TabSwitcherDailyPixelTests.swift */, + 6FF9AD402CE6610600C5A406 /* TabSwitcherOpenDailyPixelTests.swift */, 85010503292FFB080033978F /* FireproofFaviconUpdaterTests.swift */, 8565A34C1FC8DFE400239327 /* LaunchTabNotificationTests.swift */, 984D035F24AF49160066CFB8 /* TabPreviewsSourceTests.swift */, @@ -8153,7 +8153,7 @@ C1B7B53428944EFA0098FD6A /* CoreDataTestUtilities.swift in Sources */, 859DB81E2CE62766001F7210 /* TextZoomTests.swift in Sources */, 1DE384E42BC41E2500871AF6 /* PixelExperimentTests.swift in Sources */, - 6FF9AD412CE6610F00C5A406 /* TabSwitcherDailyPixelTests.swift in Sources */, + 6FF9AD412CE6610F00C5A406 /* TabSwitcherOpenDailyPixelTests.swift in Sources */, CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */, C158AC7B297AB5DC0008723A /* MockSecureVault.swift in Sources */, 569437342BE4E41500C0881B /* SyncErrorHandlerSyncErrorsAlertsTests.swift in Sources */, diff --git a/DuckDuckGo/Tab.swift b/DuckDuckGo/Tab.swift index 04e568eec6..437356124c 100644 --- a/DuckDuckGo/Tab.swift +++ b/DuckDuckGo/Tab.swift @@ -36,12 +36,20 @@ public class Tab: NSObject, NSCoding { static let link = "link" static let viewed = "viewed" static let desktop = "desktop" + static let lastViewedDate = "lastViewedDate" } private var observersHolder = [WeaklyHeldTabObserver]() let uid: String - + + /// The date last time this tab was displayed. + /// + /// - Warning: This value **must not** be used for any other purpose than for inactive tabs buckets aggregation + /// into a daily pixel in `TabSwitcherOpenDailyPixel`. If you plan to do something else, + /// read through https://app.asana.com/0/69071770703008/1208795393823862/f and reopen if necessary. + private(set) var lastViewedDate: Date? + var isDesktop: Bool = false { didSet { notifyObservers() @@ -56,6 +64,9 @@ public class Tab: NSObject, NSCoding { var viewed: Bool = false { didSet { + if viewed { + lastViewedDate = Date() + } notifyObservers() } } @@ -63,11 +74,13 @@ public class Tab: NSObject, NSCoding { public init(uid: String? = nil, link: Link? = nil, viewed: Bool = false, - desktop: Bool = AppWidthObserver.shared.isLargeWidth) { + desktop: Bool = AppWidthObserver.shared.isLargeWidth, + lastViewedDate: Date? = nil) { self.uid = uid ?? UUID().uuidString self.link = link self.viewed = viewed self.isDesktop = desktop + self.lastViewedDate = lastViewedDate } public convenience required init?(coder decoder: NSCoder) { @@ -75,7 +88,8 @@ public class Tab: NSObject, NSCoding { let link = decoder.decodeObject(forKey: NSCodingKeys.link) as? Link let viewed = decoder.containsValue(forKey: NSCodingKeys.viewed) ? decoder.decodeBool(forKey: NSCodingKeys.viewed) : true let desktop = decoder.containsValue(forKey: NSCodingKeys.desktop) ? decoder.decodeBool(forKey: NSCodingKeys.desktop) : false - self.init(uid: uid, link: link, viewed: viewed, desktop: desktop) + let lastViewedDate = decoder.containsValue(forKey: NSCodingKeys.lastViewedDate) ? decoder.decodeObject(forKey: NSCodingKeys.lastViewedDate) as? Date : nil + self.init(uid: uid, link: link, viewed: viewed, desktop: desktop, lastViewedDate: lastViewedDate) } public func encode(with coder: NSCoder) { @@ -83,6 +97,7 @@ public class Tab: NSObject, NSCoding { coder.encode(link, forKey: NSCodingKeys.link) coder.encode(viewed, forKey: NSCodingKeys.viewed) coder.encode(isDesktop, forKey: NSCodingKeys.desktop) + coder.encode(lastViewedDate, forKey: NSCodingKeys.lastViewedDate) } public override func isEqual(_ other: Any?) -> Bool { diff --git a/DuckDuckGo/TabSwitcherOpenDailyPixel.swift b/DuckDuckGo/TabSwitcherOpenDailyPixel.swift index 8b6941ceb2..c46735c497 100644 --- a/DuckDuckGo/TabSwitcherOpenDailyPixel.swift +++ b/DuckDuckGo/TabSwitcherOpenDailyPixel.swift @@ -20,11 +20,19 @@ import Foundation struct TabSwitcherOpenDailyPixel { - func parameters(with tabs: [Tab]) -> [String: String] { + /// Returns parameters with buckets for respective tabs statistics. + /// - Parameters: + /// - tabs: Tabs to be included in the statistics + /// - referenceDate: Date to be used as a reference for calculating inactive tabs. Required for testing. + func parameters(with tabs: [Tab], referenceDate: Date = .now) -> [String: String] { var parameters = [String: String]() parameters[ParameterName.tabCount] = tabCountBucket(for: tabs) parameters[ParameterName.newTabCount] = newTabCountBucket(for: tabs) + parameters[ParameterName.tabActive7dCount] = bucketForInactiveTabs(tabs, within: (-7)..., from: referenceDate) + parameters[ParameterName.tabInactive1wCount] = bucketForInactiveTabs(tabs, within: (-14)...(-8), from: referenceDate) + parameters[ParameterName.tabInactive2wCount] = bucketForInactiveTabs(tabs, within: (-21)...(-15), from: referenceDate) + parameters[ParameterName.tabInactive3wCount] = bucketForInactiveTabs(tabs, within: ...(-22), from: referenceDate) return parameters } @@ -51,6 +59,24 @@ struct TabSwitcherOpenDailyPixel { } + private func bucketForInactiveTabs(_ tabs: [Tab], within daysInterval: Range, from referenceDate: Date) -> String? where Range.Bound == Int { + let dateInterval = AbsoluteDateInterval(daysInterval: daysInterval, basedOn: referenceDate) + + let matchingTabsCount = tabs.count { + guard let lastViewedDate = $0.lastViewedDate else { return false } + + return dateInterval.contains(lastViewedDate) + } + + switch matchingTabsCount { + case 0: return "0" + case 1...5: return "1-5" + case 6...10: return "6-10" + case 11...20: return "11-20" + default: return "21+" + } + } + private func newTabCountBucket(for tabs: [Tab]) -> String? { let count = tabs.count { $0.link == nil } @@ -64,5 +90,42 @@ struct TabSwitcherOpenDailyPixel { private enum ParameterName { static let tabCount = "tab_count" static let newTabCount = "new_tab_count" + + static let tabActive7dCount = "tab_active_7d" + static let tabInactive1wCount = "tab_inactive_1w" + static let tabInactive2wCount = "tab_inactive_2w" + static let tabInactive3wCount = "tab_inactive_3w" + } +} + +private extension TimeInterval { + static let dayInterval: TimeInterval = 86400 +} + +private struct AbsoluteDateInterval where R.Bound == Int { + private let lowerBoundDate: Date + private let upperBoundDate: Date + + init(daysInterval: R, basedOn referenceDate: Date) { + switch daysInterval { + case let daysRange as ClosedRange: + self.lowerBoundDate = referenceDate.addingTimeInterval(Double(daysRange.lowerBound) * .dayInterval) + self.upperBoundDate = referenceDate.addingTimeInterval(Double(daysRange.upperBound) * .dayInterval) + + case let daysRange as PartialRangeThrough: + self.lowerBoundDate = Date.distantPast + self.upperBoundDate = referenceDate.addingTimeInterval(Double(daysRange.upperBound) * .dayInterval) + + case let daysRange as PartialRangeFrom: + self.lowerBoundDate = referenceDate.addingTimeInterval(Double(daysRange.lowerBound) * .dayInterval) + self.upperBoundDate = Date.distantFuture + + default: + fatalError("\(R.self) is not supported") + } + } + + func contains(_ date: Date) -> Bool { + lowerBoundDate...upperBoundDate ~= date } } diff --git a/DuckDuckGoTests/TabSwitcherDailyPixelTests.swift b/DuckDuckGoTests/TabSwitcherDailyPixelTests.swift deleted file mode 100644 index 7cc16a6074..0000000000 --- a/DuckDuckGoTests/TabSwitcherDailyPixelTests.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// TabSwitcherDailyPixelTests.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest -import Core -@testable import DuckDuckGo - -final class TabSwitcherDailyPixelTests: XCTestCase { - func testPopulatesParameters() { - let tabs = [Tab(), Tab(), Tab()] - let pixel = TabSwitcherOpenDailyPixel() - - let parameters = pixel.parameters(with: tabs) - - XCTAssertNotNil(parameters[ParameterName.tabCount]) - XCTAssertNotNil(parameters[ParameterName.newTabCount]) - } - - func testIncludesProperCountsForParameters() { - let tabs = [Tab(), Tab(), .mock()] - let pixel = TabSwitcherOpenDailyPixel() - - let parameters = pixel.parameters(with: tabs) - - XCTAssertEqual(parameters[ParameterName.tabCount], "2-5") - XCTAssertEqual(parameters[ParameterName.newTabCount], "2-10") - } - - func testBucketsAggregation() { - let bucketValues = [ - 1...1: "1", - 2...5: "2-5", - 6...10: "6-10", - 11...20: "11-20", - 21...40: "21-40", - 41...60: "41-60", - 61...80: "61-80", - 81...100: "81-100", - 101...125: "101-125", - 126...150: "126-150", - 151...250: "151-250", - 251...500: "251-500", - 501...504: "501+"] - - for bucket in bucketValues { - for value in bucket.key { - let tabs = Array(repeating: Tab.mock(), count: value) - - let countParameter = TabSwitcherOpenDailyPixel().parameters(with: tabs)[ParameterName.tabCount] - - XCTAssertEqual(countParameter, bucket.value) - } - } - } - - func testNewTabBucketsAggregation() { - let bucketValues = [ - 0...1: "0-1", - 2...10: "2-10", - 11...20: "11+"] - - for bucket in bucketValues { - for value in bucket.key { - let tabs = Array(repeating: Tab(), count: value) - - let countParameter = TabSwitcherOpenDailyPixel().parameters(with: tabs)[ParameterName.newTabCount] - - XCTAssertEqual(countParameter, bucket.value) - } - } - } -} - -private extension Tab { - static func mock() -> Tab { - Tab(link: Link(title: nil, url: URL("https://example.com")!)) - } -} - -private enum ParameterName { - static let newTabCount = "new_tab_count" - static let tabCount = "tab_count" -} diff --git a/DuckDuckGoTests/TabSwitcherOpenDailyPixelTests.swift b/DuckDuckGoTests/TabSwitcherOpenDailyPixelTests.swift new file mode 100644 index 0000000000..9d5d9c984c --- /dev/null +++ b/DuckDuckGoTests/TabSwitcherOpenDailyPixelTests.swift @@ -0,0 +1,257 @@ +// +// TabSwitcherOpenDailyPixelTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Core +@testable import DuckDuckGo + +final class TabSwitcherOpenDailyPixelTests: XCTestCase { + func testPopulatesParameters() { + let tabs = [Tab(), Tab(), Tab()] + let pixel = TabSwitcherOpenDailyPixel() + + let parameters = pixel.parameters(with: tabs) + + XCTAssertNotNil(parameters[ParameterName.tabCount]) + XCTAssertNotNil(parameters[ParameterName.newTabCount]) + } + + func testIncludesProperCountsForParameters() { + let tabs = [Tab(), Tab(), .mock()] + let pixel = TabSwitcherOpenDailyPixel() + + let parameters = pixel.parameters(with: tabs) + + XCTAssertEqual(parameters[ParameterName.tabCount], "2-5") + XCTAssertEqual(parameters[ParameterName.newTabCount], "2-10") + } + + func testBucketsAggregation() { + let bucketValues = [ + 1...1: "1", + 2...5: "2-5", + 6...10: "6-10", + 11...20: "11-20", + 21...40: "21-40", + 41...60: "41-60", + 61...80: "61-80", + 81...100: "81-100", + 101...125: "101-125", + 126...150: "126-150", + 151...250: "151-250", + 251...500: "251-500", + 501...504: "501+"] + + for bucket in bucketValues { + for value in bucket.key { + let tabs = Array(repeating: Tab.mock(), count: value) + + let countParameter = TabSwitcherOpenDailyPixel().parameters(with: tabs)[ParameterName.tabCount] + + XCTAssertEqual(countParameter, bucket.value) + } + } + } + + func testNewTabBucketsAggregation() { + let bucketValues = [ + 0...1: "0-1", + 2...10: "2-10", + 11...20: "11+"] + + for bucket in bucketValues { + for value in bucket.key { + let tabs = Array(repeating: Tab(), count: value) + + let countParameter = TabSwitcherOpenDailyPixel().parameters(with: tabs)[ParameterName.newTabCount] + + XCTAssertEqual(countParameter, bucket.value) + } + } + } + + // - MARK: Inactive tabs aggregation tests + + func testTabsWithoutLastVisitValueArentIncludedInBuckets() throws { + let tabs = [Tab.mock(), .mock()] + + let parameters = TabSwitcherOpenDailyPixel().parameters(with: tabs) + + try testBucketParameters(parameters, expectedCount: 0) + } + + func testEdgeCaseBucketParameterForInactiveTabs() throws { + let now = Date() + + let tabs: [Tab] = [ + .mock(lastViewedDate: now.daysAgo(7)), + .mock(lastViewedDate: now.daysAgo(14)), + .mock(lastViewedDate: now.daysAgo(21)), + .mock(lastViewedDate: now.daysAgo(22)) + ] + + let pixelParametersForSecondInterval = TabSwitcherOpenDailyPixel().parameters(with: tabs, referenceDate: now) + + try testBucketParameters(pixelParametersForSecondInterval, expectedCount: 1) + } + + func testBucketParametersForInactiveTabs() throws { + let now = Date() + + let tabsSecondInterval = Tab.stubCollectionForSecondInterval(baseDate: now) + let parametersForSecondInterval = TabSwitcherOpenDailyPixel().parameters(with: tabsSecondInterval, referenceDate: now) + + let tabsThirdInterval = Tab.stubCollectionForThirdInterval(baseDate: now) + let parametersForThirdInterval = TabSwitcherOpenDailyPixel().parameters(with: tabsThirdInterval, referenceDate: now) + + try testBucketParameters(parametersForSecondInterval, expectedCount: 5) + try testBucketParameters(parametersForThirdInterval, expectedCount: 6) + } + + func testBucketNamingForInactiveTabs() throws { + let now = Date() + let expectedBuckets = [ + 0...0: "0", + 1...5: "1-5", + 6...10: "6-10", + 11...20: "11-20", + 21...40: "21+" + ] + + // How many days need to pass for each interval bucket + let parameterDaysOffsetMapping = [ + ParameterName.tabActive7dCount: 0, + ParameterName.tabInactive1wCount: 8, + ParameterName.tabInactive2wCount: 15, + ParameterName.tabInactive3wCount: 22 + ] + + for bucket in expectedBuckets { + let count = bucket.key.lowerBound + + for parameter in parameterDaysOffsetMapping { + let daysOffset = parameter.value + // Create tabs based on expected count for bucket, using proper days offset + let tabs = Array(repeating: Tab.mock(lastViewedDate: now.daysAgo(daysOffset)), count: count) + + let parameters = TabSwitcherOpenDailyPixel().parameters(with: tabs, referenceDate: now) + + XCTAssertEqual(parameters[parameter.key], bucket.value, "Failed for bucket: \(bucket.key) with parameter: \(parameter.key)") + } + } + } + + // MARK: - Test helper methods + + private func testBucketParameters(_ parameters: [String: String], expectedCount: Int) throws { + let parameterNames = [ + ParameterName.tabActive7dCount, + ParameterName.tabInactive1wCount, + ParameterName.tabInactive2wCount, + ParameterName.tabInactive3wCount + ] + + let expectedBucket = try XCTUnwrap(Buckets.inactiveTabs.first { $0.key.contains(expectedCount) }).value + for parameterName in parameterNames { + let bucketValue = parameters[parameterName] + + XCTAssertEqual(bucketValue, expectedBucket, "Failed for parameter: \(parameterName)") + } + } +} + +private extension Tab { + static func mock(lastViewedDate: Date? = nil) -> Tab { + Tab(link: Link(title: nil, url: URL("https://example.com")!), lastViewedDate: lastViewedDate) + } + + static func stubCollectionForSecondInterval(baseDate: Date) -> [Tab] { + [ + // MARK: First week + .mock(lastViewedDate: baseDate), + .mock(lastViewedDate: baseDate.daysAgo(3)), + .mock(lastViewedDate: baseDate.daysAgo(4)), + .mock(lastViewedDate: baseDate.daysAgo(5)), + .mock(lastViewedDate: baseDate.daysAgo(7)), + + // MARK: >1 week + .mock(lastViewedDate: baseDate.daysAgo(8)), + .mock(lastViewedDate: baseDate.daysAgo(10)), + .mock(lastViewedDate: baseDate.daysAgo(11)), + .mock(lastViewedDate: baseDate.daysAgo(12)), + .mock(lastViewedDate: baseDate.daysAgo(14)), + + // MARK: >2 weeks + .mock(lastViewedDate: baseDate.daysAgo(15)), + .mock(lastViewedDate: baseDate.daysAgo(16)), + .mock(lastViewedDate: baseDate.daysAgo(17)), + .mock(lastViewedDate: baseDate.daysAgo(18)), + .mock(lastViewedDate: baseDate.daysAgo(21)), + + // MARK: >3 weeks + .mock(lastViewedDate: baseDate.daysAgo(22)), + .mock(lastViewedDate: baseDate.daysAgo(23)), + .mock(lastViewedDate: baseDate.daysAgo(24)), + .mock(lastViewedDate: baseDate.daysAgo(100)), + .mock(lastViewedDate: Date.distantPast), + ] + } + static func stubCollectionForThirdInterval(baseDate: Date) -> [Tab] { + stubCollectionForSecondInterval(baseDate: baseDate) + + [ + // MARK: First week + .mock(lastViewedDate: baseDate.daysAgo(4)), + + // MARK: >1 week + .mock(lastViewedDate: baseDate.daysAgo(14)), + + // MARK: >2 weeks + .mock(lastViewedDate: baseDate.daysAgo(15)), + + // MARK: >3 weeks + .mock(lastViewedDate: baseDate.daysAgo(22)) + ] + } +} + +private enum ParameterName { + static let newTabCount = "new_tab_count" + static let tabCount = "tab_count" + + static let tabActive7dCount = "tab_active_7d" + static let tabInactive1wCount = "tab_inactive_1w" + static let tabInactive2wCount = "tab_inactive_2w" + static let tabInactive3wCount = "tab_inactive_3w" +} + +private enum Buckets { + static let inactiveTabs = [ + 0...0: "0", + 1...5: "1-5", + 6...10: "6-10", + 11...20: "11-20", + 21...40: "21+" + ] +} + +private extension Date { + func daysAgo(_ days: Int) -> Date { + addingTimeInterval(TimeInterval(-days * 86400)) + } +} From b42de066c5de26deb5c77a5a1fb14cc90fb79fb3 Mon Sep 17 00:00:00 2001 From: Bartek Waresiak Date: Fri, 29 Nov 2024 13:19:53 +0100 Subject: [PATCH 04/12] Remove Special Pages tests --- DuckDuckGo.xcodeproj/project.pbxproj | 4 - DuckDuckGoTests/SpecialErrorPageTests.swift | 304 -------------------- 2 files changed, 308 deletions(-) delete mode 100644 DuckDuckGoTests/SpecialErrorPageTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3273c57da7..6b1fa683d5 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -982,7 +982,6 @@ CBAA195A27BFE15600A4BD49 /* NSManagedObjectContextExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */; }; CBC83E3429B631780008E19C /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = CBC83E3329B631780008E19C /* Configuration */; }; CBC88EE12C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */; }; - CBC88EE32C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE22C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift */; }; CBC88EE52C8097B500F0F8C5 /* URLCredentialCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */; }; CBCCF96828885DEE006F4A71 /* AppPrivacyConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C4BC3127C3F9B600C40026 /* AppPrivacyConfigurationTests.swift */; }; CBD4F13C279EBF4A00B20FD7 /* HomeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */; }; @@ -2817,7 +2816,6 @@ CBB6B2542AF6D543006B777C /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = ""; }; CBC7AB542AF6D583008CB798 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScriptTests.swift; sourceTree = ""; }; - CBC88EE22C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageTests.swift; sourceTree = ""; }; CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLCredentialCreator.swift; sourceTree = ""; }; CBC8DC252AF6D4CD00BA681A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessage.swift; sourceTree = ""; }; @@ -6001,7 +5999,6 @@ F189AED61F18F6DE001EBAE1 /* TabTests.swift */, D625AAEA2BBEEFC900BC189A /* TabURLInterceptorTests.swift */, CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */, - CBC88EE22C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift */, ); name = Tabs; sourceTree = ""; @@ -8157,7 +8154,6 @@ CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */, C158AC7B297AB5DC0008723A /* MockSecureVault.swift in Sources */, 569437342BE4E41500C0881B /* SyncErrorHandlerSyncErrorsAlertsTests.swift in Sources */, - CBC88EE32C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift in Sources */, 85C11E4120904BBE00BFFEB4 /* VariantManagerTests.swift in Sources */, F1134ECE1F40EA9C00B73467 /* AtbParserTests.swift in Sources */, F189AEE41F18FDAF001EBAE1 /* LinkTests.swift in Sources */, diff --git a/DuckDuckGoTests/SpecialErrorPageTests.swift b/DuckDuckGoTests/SpecialErrorPageTests.swift deleted file mode 100644 index f3ff737d3b..0000000000 --- a/DuckDuckGoTests/SpecialErrorPageTests.swift +++ /dev/null @@ -1,304 +0,0 @@ -// -// SpecialErrorPageTests.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import XCTest -import WebKit - -@testable import SpecialErrorPages -@testable import DuckDuckGo - -class MockSpecialErrorWebView: WKWebView { - - var loadRequestHandler: ((URLRequest, String) -> Void)? - var currentURL: URL? - - override func loadSimulatedRequest(_ request: URLRequest, responseHTML string: String) -> WKNavigation { - loadRequestHandler?(request, string) - return super.loadSimulatedRequest(request, responseHTML: string) - } - - override var url: URL? { - return currentURL - } - - func setCurrentURL(_ url: URL) { - self.currentURL = url - } - -} - -final class SpecialErrorPageTests: XCTestCase { - - var webView: MockSpecialErrorWebView! - var sut: TabViewController! - - override func setUpWithError() throws { - throw XCTSkip("Potentially Flaky") - - try super.setUpWithError() - let featureFlagger = MockFeatureFlagger() - featureFlagger.enabledFeatureFlags = [.sslCertificatesBypass] - sut = .fake(customWebView: { [weak self] configuration in - guard let self else { fatalError("It has to exist") } - self.webView = MockSpecialErrorWebView(frame: CGRect(), configuration: configuration) - return self.webView - }, featureFlagger: featureFlagger) - WKNavigation.swizzleDealloc() - } - - override func tearDown() async throws { - try await super.tearDown() - WKNavigation.restoreDealloc() - } - - func testWhenCertificateExpiredThenExpectedErrorPageIsShown() { - // GIVEN - let error = NSError(domain: "test", - code: NSURLErrorServerCertificateUntrusted, - userInfo: ["_kCFStreamErrorCodeKey": errSSLCertExpired, - NSURLErrorFailingURLErrorKey: URL(string: "https://expired.badssl.com")!]) - let expectation = self.expectation(description: "Special error page should be loaded") - var didFulfill = false - webView.loadRequestHandler = { request, html in - if !didFulfill { - XCTAssertTrue(html.contains("Warning: This site may be insecure")) - XCTAssertTrue(html.contains("is expired")) - XCTAssertEqual(request.url!.host, URL(string: "https://expired.badssl.com")!.host) - expectation.fulfill() - didFulfill = true - } - } - - // WHEN - sut.webView(webView, didFailProvisionalNavigation: WKNavigation(), withError: error) - - // THEN - XCTAssertEqual(sut.failedURL, URL(string: "https://expired.badssl.com")!) - XCTAssertEqual(sut.errorData, SpecialErrorData(kind: .ssl, - errorType: "expired", - domain: "expired.badssl.com", - eTldPlus1: "badssl.com")) - waitForExpectations(timeout: 1) { error in - XCTAssertNil(error, "Expectation was not fulfilled in time") - } - } - - func testWhenCertificateWrongHostThenExpectedErrorPageIsShown() { - // GIVEN - let error = NSError(domain: "test", - code: NSURLErrorServerCertificateUntrusted, - userInfo: ["_kCFStreamErrorCodeKey": errSSLHostNameMismatch, - NSURLErrorFailingURLErrorKey: URL(string: "https://wrong.host.badssl.com")!]) - let expectation = self.expectation(description: "Special error page should be loaded") - var didFulfill = false - webView.loadRequestHandler = { request, html in - if !didFulfill { - XCTAssertTrue(html.contains("Warning: This site may be insecure")) - XCTAssertTrue(html.contains("does not match")) - XCTAssertEqual(request.url!.host, URL(string: "https://wrong.host.badssl.com")!.host) - expectation.fulfill() - didFulfill = true - } - } - - // WHEN - sut.webView(webView, didFailProvisionalNavigation: WKNavigation(), withError: error) - - // THEN - XCTAssertEqual(sut.failedURL, URL(string: "https://wrong.host.badssl.com")!) - XCTAssertEqual(sut.errorData, SpecialErrorData(kind: .ssl, - errorType: "wrongHost", - domain: "wrong.host.badssl.com", - eTldPlus1: "badssl.com")) - waitForExpectations(timeout: 1) { error in - XCTAssertNil(error, "Expectation was not fulfilled in time") - } - } - - func testWhenCertificateSelfSignedThenExpectedErrorPageIsShown() { - // GIVEN - let error = NSError(domain: "test", - code: NSURLErrorServerCertificateUntrusted, - userInfo: ["_kCFStreamErrorCodeKey": errSSLXCertChainInvalid, - NSURLErrorFailingURLErrorKey: URL(string: "https://self-signed.badssl.com")!]) - let expectation = self.expectation(description: "Special error page should be loaded") - var didFulfill = false - webView.loadRequestHandler = { request, html in - if !didFulfill { - XCTAssertTrue(html.contains("Warning: This site may be insecure")) - XCTAssertTrue(html.contains("is not trusted")) - XCTAssertEqual(request.url!.host, URL(string: "https://self-signed.badssl.com")!.host) - expectation.fulfill() - didFulfill = true - } - } - - // WHEN - sut.webView(webView, didFailProvisionalNavigation: WKNavigation(), withError: error) - - // THEN - XCTAssertEqual(sut.failedURL, URL(string: "https://self-signed.badssl.com")!) - XCTAssertEqual(sut.errorData, SpecialErrorData(kind: .ssl, - errorType: "selfSigned", - domain: "self-signed.badssl.com", - eTldPlus1: "badssl.com")) - waitForExpectations(timeout: 1) { error in - XCTAssertNil(error, "Expectation was not fulfilled in time") - } - } - - func testWhenOtherCertificateIssueThenExpectedErrorPageIsShown() { - // GIVEN - let error = NSError(domain: "test", - code: NSURLErrorServerCertificateUntrusted, - userInfo: ["_kCFStreamErrorCodeKey": errSSLUnknownRootCert, - NSURLErrorFailingURLErrorKey: URL(string: "https://untrusted-root.badssl.com")!]) - let expectation = self.expectation(description: "Special error page should be loaded") - var didFulfill = false - webView.loadRequestHandler = { request, html in - if !didFulfill { - XCTAssertTrue(html.contains("Warning: This site may be insecure")) - XCTAssertTrue(html.contains("is not trusted")) - XCTAssertEqual(request.url!.host, URL(string: "https://untrusted-root.badssl.com")!.host) - expectation.fulfill() - didFulfill = true - } - } - - // WHEN - sut.webView(webView, didFailProvisionalNavigation: WKNavigation(), withError: error) - - // THEN - XCTAssertEqual(sut.failedURL, URL(string: "https://untrusted-root.badssl.com")!) - XCTAssertEqual(sut.errorData, SpecialErrorData(kind: .ssl, - errorType: "invalid", - domain: "untrusted-root.badssl.com", - eTldPlus1: "badssl.com")) - waitForExpectations(timeout: 1) { error in - XCTAssertNil(error, "Expectation was not fulfilled in time") - } - } - - @MainActor - func testWhenNavigationEndedIfNoSSLFailureSSLUserScriptIsNotEnabled() { - // GIVEN - webView.setCurrentURL(URL(string: "https://self-signed.badssl.com")!) - sut.storedSpecialErrorPageUserScript = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") - - // WHEN - sut.webView(webView, didFinish: WKNavigation()) - - // THEN - XCTAssertFalse(sut.specialErrorPageUserScript?.isEnabled ?? true) - } - - @MainActor - func testWhenNavigationEndedIfSSLFailureButURLIsDifferentFromNavigationURLThenSSLUserScriptIsNotEnabled() { - // GIVEN - webView.setCurrentURL(URL(string: "https://self-signed.badssl.com")!) - sut.failedURL = URL(string: "https://different.url.com")! - sut.storedSpecialErrorPageUserScript = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") - - // WHEN - sut.webView(webView, didFinish: WKNavigation()) - - // THEN - XCTAssertFalse(sut.specialErrorPageUserScript?.isEnabled ?? true) - } - - @MainActor - func testWhenNavigationEndedIfSSLFailureAndNavigationURLIsTheSameAsFailingURLThenSSLUserScriptIsEnabled() { - // GIVEN - webView.setCurrentURL(URL(string: "https://self-signed.badssl.com")!) - sut.failedURL = URL(string: "https://self-signed.badssl.com")! - sut.storedSpecialErrorPageUserScript = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") - - // WHEN - sut.webView(webView, didFinish: WKNavigation()) - - // THEN - XCTAssertTrue(sut.specialErrorPageUserScript?.isEnabled ?? false) - } - - func testWhenDidReceiveChallengeIfChallengeForCertificateValidationAndNoBypassThenShouldNotReturnCredentials() async { - let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) - let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallengeSender()) - await sut.webView(webView, didReceive: challenge) { _, credential in - XCTAssertNil(credential) - } - } - - func testWhenDidReceiveChallengeIfChallengeForCertificateValidationAndUserRequestBypassThenReturnsCredentials() async { - let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) - let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallengeSender()) - await sut.visitSite() - await sut.webView(webView, didReceive: challenge) { _, credential in - XCTAssertNotNil(credential) - } - } - -} - -final class ChallengeSender: URLAuthenticationChallengeSender { - func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {} - func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {} - func cancel(_ challenge: URLAuthenticationChallenge) {} - func isEqual(_ object: Any?) -> Bool { - return false - } - var hash: Int = 0 - var superclass: AnyClass? - func `self`() -> Self { - self - } - func perform(_ aSelector: Selector!) -> Unmanaged! { - return nil - } - func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged! { - return nil - } - func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged! { - return nil - } - func isProxy() -> Bool { - return false - } - func isKind(of aClass: AnyClass) -> Bool { - return false - } - func isMember(of aClass: AnyClass) -> Bool { - return false - } - func conforms(to aProtocol: Protocol) -> Bool { - return false - } - func responds(to aSelector: Selector!) -> Bool { - return false - } - var description: String = "" -} - -final class MockCredentialCreator: URLCredentialCreating { - - func urlCredentialFrom(trust: SecTrust?) -> URLCredential? { - return URLCredential(user: "", password: "", persistence: .forSession) - } - -} From 620844ea5e8cfa6a9a768b84c0e59eff28983dee Mon Sep 17 00:00:00 2001 From: Bartek Waresiak Date: Fri, 29 Nov 2024 13:22:15 +0100 Subject: [PATCH 05/12] Re-enable pixel tests except for flaky one --- DuckDuckGoTests/PixelTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGoTests/PixelTests.swift b/DuckDuckGoTests/PixelTests.swift index 132517dac6..2d4ee61a94 100644 --- a/DuckDuckGoTests/PixelTests.swift +++ b/DuckDuckGoTests/PixelTests.swift @@ -30,7 +30,6 @@ class PixelTests: XCTestCase { let userAgentName = "User-Agent" override func setUpWithError() throws { - throw XCTSkip("Potentially flaky") try super.setUpWithError() Pixel.isDryRun = false @@ -162,7 +161,8 @@ class PixelTests: XCTestCase { wait(for: [expectation], timeout: 5.0) } - func testPixelDebouncePreventsFiringWithinInterval() { + func testPixelDebouncePreventsFiringWithinInterval() throws { + throw XCTSkip("Flaky") let firstFireExpectation = XCTestExpectation(description: "First pixel fire should succeed") let thirdFireExpectation = XCTestExpectation(description: "Third pixel fire should succeed after debounce interval") From 4a3c4e8eea03a645fbae7c836cea4a2d718fd59c Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 29 Nov 2024 13:22:15 +0100 Subject: [PATCH 06/12] Client displays correct subscription (#3620) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1208524871249522/1208379950230747/f **Description**: See https://app.asana.com/0/1208524871249522/1208799981662317/f **Steps to test this PR**: See https://app.asana.com/0/0/1208836865988482/f and its parent task. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- Core/FeatureFlag.swift | 16 ++- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +- DuckDuckGo/AppDependencyProvider.swift | 28 ++++- DuckDuckGo/NetworkProtectionRootView.swift | 4 +- DuckDuckGo/NetworkProtectionStatusView.swift | 4 +- .../NetworkProtectionStatusViewModel.swift | 5 +- DuckDuckGo/SettingsOthersView.swift | 6 +- DuckDuckGo/SettingsState.swift | 2 + DuckDuckGo/SettingsSubscriptionView.swift | 101 ++++++++++++------ DuckDuckGo/SettingsViewModel.swift | 5 +- .../UnifiedFeedbackFormViewModel.swift | 20 +++- .../Feedback/UnifiedFeedbackRootView.swift | 22 ++-- DuckDuckGo/Subscription/Subscription.swift | 25 ----- ...scriptionPagesUseSubscriptionFeature.swift | 15 +-- .../SubscriptionEmailViewModel.swift | 8 +- .../ViewModel/SubscriptionFlowViewModel.swift | 8 +- .../Views/SubscriptionSettingsView.swift | 4 +- .../SubscriptionDebugViewController.swift | 38 ++++++- DuckDuckGo/UserText.swift | 1 + DuckDuckGo/en.lproj/Localizable.strings | 3 + ...etworkProtectionStatusViewModelTests.swift | 12 ++- .../StorePurchaseManagerTests.swift | 5 +- .../SubscriptionContainerViewModelTests.swift | 4 +- .../SubscriptionFlowViewModelTests.swift | 6 +- ...tionPagesUseSubscriptionFeatureTests.swift | 22 ++-- 26 files changed, 254 insertions(+), 118 deletions(-) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index da04bcfaf6..796c0f56fb 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -56,11 +56,21 @@ public enum FeatureFlag: String { /// https://app.asana.com/0/1208592102886666/1208613627589762/f case crashReportOptInStatusResetting + case isPrivacyProLaunchedROW + case isPrivacyProLaunchedROWOverride } extension FeatureFlag: FeatureFlagDescribing { + + public static var localOverrideStoreName: String = "com.duckduckgo.app.featureFlag.localOverrides" + public var supportsLocalOverriding: Bool { - false + switch self { + case .isPrivacyProLaunchedROWOverride: + return true + default: + return false + } } public var source: FeatureFlagSource { @@ -123,6 +133,10 @@ extension FeatureFlag: FeatureFlagDescribing { return .remoteReleasable(.feature(.adAttributionReporting)) case .crashReportOptInStatusResetting: return .internalOnly + case .isPrivacyProLaunchedROW: + return .remoteReleasable(.subfeature(PrivacyProSubfeature.isLaunchedROW)) + case .isPrivacyProLaunchedROWOverride: + return .remoteReleasable(.subfeature(PrivacyProSubfeature.isLaunchedROWOverride)) } } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3800ace64b..0250667290 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -11054,7 +11054,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 211.1.3; + version = "211.1.3-1"; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4e271933b0..5ef0b5dc49 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" : "f83b1f5ebd328bc2447d1a3793149bb21037d685", - "version" : "211.1.3" + "revision" : "114cdbfcfae15ad8c7d5e502832e94061aef7cff", + "version" : "211.1.3-1" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 245901da44..b378ab86e9 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -94,7 +94,12 @@ final class AppDependencyProvider: DependencyProvider { private init() { featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, - privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager) + privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, + localOverrides: FeatureFlagLocalOverrides( + keyValueStore: UserDefaults(suiteName: FeatureFlag.localOverrideStoreName)!, + actionHandler: FeatureFlagOverridesPublishingHandler() + ), + for: FeatureFlag.self) configurationManager = ConfigurationManager(store: configurationStore) @@ -109,16 +114,33 @@ final class AppDependencyProvider: DependencyProvider { let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) let authService = DefaultAuthEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let subscriptionFeatureMappingCache = DefaultSubscriptionFeatureMappingCache(subscriptionEndpointService: subscriptionService, + userDefaults: subscriptionUserDefaults) + let accountManager = DefaultAccountManager(accessTokenStorage: accessTokenStorage, entitlementsCache: entitlementsCache, subscriptionEndpointService: subscriptionService, authEndpointService: authService) - let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: DefaultStorePurchaseManager(), + let theFeatureFlagger = featureFlagger + let subscriptionFeatureFlagger: FeatureFlaggerMapping = FeatureFlaggerMapping { feature in + switch feature { + case .isLaunchedROW: + return theFeatureFlagger.isFeatureOn(.isPrivacyProLaunchedROW) + case .isLaunchedROWOverride: + return theFeatureFlagger.isFeatureOn(.isPrivacyProLaunchedROWOverride) + default: + return feature.defaultState + } + } + + let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionFeatureMappingCache), accountManager: accountManager, subscriptionEndpointService: subscriptionService, authEndpointService: authService, - subscriptionEnvironment: subscriptionEnvironment) + subscriptionFeatureMappingCache: subscriptionFeatureMappingCache, + subscriptionEnvironment: subscriptionEnvironment, + subscriptionFeatureFlagger: subscriptionFeatureFlagger) accountManager.delegate = subscriptionManager self.subscriptionManager = subscriptionManager diff --git a/DuckDuckGo/NetworkProtectionRootView.swift b/DuckDuckGo/NetworkProtectionRootView.swift index 974c03022c..357e302c3d 100644 --- a/DuckDuckGo/NetworkProtectionRootView.swift +++ b/DuckDuckGo/NetworkProtectionRootView.swift @@ -27,6 +27,7 @@ struct NetworkProtectionRootView: View { let statusViewModel: NetworkProtectionStatusViewModel init() { + let subscriptionManager = AppDependencyProvider.shared.subscriptionManager let accountManager = AppDependencyProvider.shared.subscriptionManager.accountManager let locationListRepository = NetworkProtectionLocationListCompositeRepository(accountManager: accountManager) let usesUnifiedFeedbackForm = accountManager.isUserAuthenticated @@ -35,7 +36,8 @@ struct NetworkProtectionRootView: View { statusObserver: AppDependencyProvider.shared.connectionObserver, serverInfoObserver: AppDependencyProvider.shared.serverInfoObserver, locationListRepository: locationListRepository, - usesUnifiedFeedbackForm: usesUnifiedFeedbackForm) + usesUnifiedFeedbackForm: usesUnifiedFeedbackForm, + subscriptionManager: subscriptionManager) } var body: some View { diff --git a/DuckDuckGo/NetworkProtectionStatusView.swift b/DuckDuckGo/NetworkProtectionStatusView.swift index 88e6768dea..f8a51bf3db 100644 --- a/DuckDuckGo/NetworkProtectionStatusView.swift +++ b/DuckDuckGo/NetworkProtectionStatusView.swift @@ -309,7 +309,9 @@ struct NetworkProtectionStatusView: View { @ViewBuilder private func about() -> some View { - let viewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), source: .vpn) + let viewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), + source: .vpn, + subscriptionManager: statusModel.subscriptionManager) Section { NavigationLink(UserText.netPVPNSettingsFAQ, destination: LazyView(NetworkProtectionFAQView())) diff --git a/DuckDuckGo/NetworkProtectionStatusViewModel.swift b/DuckDuckGo/NetworkProtectionStatusViewModel.swift index becccb79aa..566c697227 100644 --- a/DuckDuckGo/NetworkProtectionStatusViewModel.swift +++ b/DuckDuckGo/NetworkProtectionStatusViewModel.swift @@ -176,6 +176,7 @@ final class NetworkProtectionStatusViewModel: ObservableObject { @Published public var animationsOn: Bool = false public let usesUnifiedFeedbackForm: Bool + public let subscriptionManager: SubscriptionManager public init(tunnelController: (TunnelController & TunnelSessionProvider), settings: VPNSettings, @@ -183,13 +184,15 @@ final class NetworkProtectionStatusViewModel: ObservableObject { serverInfoObserver: ConnectionServerInfoObserver, errorObserver: ConnectionErrorObserver = ConnectionErrorObserverThroughSession(), locationListRepository: NetworkProtectionLocationListRepository, - usesUnifiedFeedbackForm: Bool) { + usesUnifiedFeedbackForm: Bool, + subscriptionManager: SubscriptionManager) { self.tunnelController = tunnelController self.settings = settings self.statusObserver = statusObserver self.serverInfoObserver = serverInfoObserver self.errorObserver = errorObserver self.usesUnifiedFeedbackForm = usesUnifiedFeedbackForm + self.subscriptionManager = subscriptionManager statusMessage = Self.message(for: statusObserver.recentValue) self.headerTitle = Self.titleText(status: statusObserver.recentValue) diff --git a/DuckDuckGo/SettingsOthersView.swift b/DuckDuckGo/SettingsOthersView.swift index 96f3cab8d1..1e3202ec6e 100644 --- a/DuckDuckGo/SettingsOthersView.swift +++ b/DuckDuckGo/SettingsOthersView.swift @@ -34,9 +34,11 @@ struct SettingsOthersView: View { // Share Feedback if viewModel.usesUnifiedFeedbackForm { - let formViewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), source: .settings) + let formViewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), + source: .settings, + subscriptionManager: viewModel.subscriptionManager) NavigationLink { - UnifiedFeedbackCategoryView(UserText.subscriptionFeedback, sources: UnifiedFeedbackFlowCategory.self, selection: $viewModel.selectedFeedbackFlow) { + UnifiedFeedbackCategoryView(UserText.subscriptionFeedback, options: UnifiedFeedbackFlowCategory.allCases, selection: $viewModel.selectedFeedbackFlow) { if let selectedFeedbackFlow = viewModel.selectedFeedbackFlow { switch UnifiedFeedbackFlowCategory(rawValue: selectedFeedbackFlow) { case nil: diff --git a/DuckDuckGo/SettingsState.swift b/DuckDuckGo/SettingsState.swift index 665077e29d..6d1fb7c43d 100644 --- a/DuckDuckGo/SettingsState.swift +++ b/DuckDuckGo/SettingsState.swift @@ -43,6 +43,7 @@ struct SettingsState { var hasActiveSubscription: Bool var isRestoring: Bool var shouldDisplayRestoreSubscriptionError: Bool + var subscriptionFeatures: [Entitlement.ProductName] var entitlements: [Entitlement.ProductName] var platform: DDGSubscription.Platform var isShowingStripeView: Bool @@ -132,6 +133,7 @@ struct SettingsState { hasActiveSubscription: false, isRestoring: false, shouldDisplayRestoreSubscriptionError: false, + subscriptionFeatures: [], entitlements: [], platform: .unknown, isShowingStripeView: false), diff --git a/DuckDuckGo/SettingsSubscriptionView.swift b/DuckDuckGo/SettingsSubscriptionView.swift index b2414c2855..7c6ce225eb 100644 --- a/DuckDuckGo/SettingsSubscriptionView.swift +++ b/DuckDuckGo/SettingsSubscriptionView.swift @@ -64,9 +64,19 @@ struct SettingsSubscriptionView: View { @ViewBuilder private var purchaseSubscriptionView: some View { Group { + let subtitleText = { + switch subscriptionManager.storePurchaseManager().currentStorefrontRegion { + case .usa: + UserText.settingsPProDescription + case .restOfWorld: + UserText.settingsPProROWDescription + } + }() + SettingsCellView(label: UserText.settingsPProSubscribe, - subtitle: UserText.settingsPProDescription, + subtitle: subtitleText, image: Image("SettingsPrivacyPro")) + .disabled(true) // Get privacy pro SettingsCustomCell(content: { @@ -93,23 +103,33 @@ struct SettingsSubscriptionView: View { @ViewBuilder private var disabledFeaturesView: some View { - SettingsCellView(label: UserText.settingsPProVPNTitle, - image: Image("SettingsPrivacyProVPN"), - statusIndicator: StatusIndicatorView(status: .off), - isGreyedOut: true - ) - SettingsCellView( - label: UserText.settingsPProDBPTitle, - image: Image("SettingsPrivacyProPIR"), - statusIndicator: StatusIndicatorView(status: .off), - isGreyedOut: true - ) - SettingsCellView( - label: UserText.settingsPProITRTitle, - image: Image("SettingsPrivacyProITP"), - statusIndicator: StatusIndicatorView(status: .off), - isGreyedOut: true - ) + let subscriptionFeatures = settingsViewModel.state.subscription.subscriptionFeatures + + if subscriptionFeatures.contains(.networkProtection) { + SettingsCellView(label: UserText.settingsPProVPNTitle, + image: Image("SettingsPrivacyProVPN"), + statusIndicator: StatusIndicatorView(status: .off), + isGreyedOut: true + ) + } + + if subscriptionFeatures.contains(.dataBrokerProtection) { + SettingsCellView( + label: UserText.settingsPProDBPTitle, + image: Image("SettingsPrivacyProPIR"), + statusIndicator: StatusIndicatorView(status: .off), + isGreyedOut: true + ) + } + + if subscriptionFeatures.contains(.identityTheftRestoration) || subscriptionFeatures.contains(.identityTheftRestorationGlobal) { + SettingsCellView( + label: UserText.settingsPProITRTitle, + image: Image("SettingsPrivacyProITP"), + statusIndicator: StatusIndicatorView(status: .off), + isGreyedOut: true + ) + } } @ViewBuilder @@ -155,37 +175,50 @@ struct SettingsSubscriptionView: View { @ViewBuilder private var subscriptionDetailsView: some View { - - if settingsViewModel.state.subscription.entitlements.contains(.networkProtection) { + let subscriptionFeatures = settingsViewModel.state.subscription.subscriptionFeatures + let userEntitlements = settingsViewModel.state.subscription.entitlements + + if subscriptionFeatures.contains(.networkProtection) { + let hasVPNEntitlement = userEntitlements.contains(.networkProtection) + let isVPNConnected = settingsViewModel.state.networkProtectionConnected + NavigationLink(destination: LazyView(NetworkProtectionRootView()), isActive: $isShowingVPN) { SettingsCellView( label: UserText.settingsPProVPNTitle, image: Image("SettingsPrivacyProVPN"), - statusIndicator: StatusIndicatorView(status: settingsViewModel.state.networkProtectionConnected ? .on : .off) + statusIndicator: StatusIndicatorView(status: isVPNConnected ? .on : .off), + isGreyedOut: !hasVPNEntitlement ) } + .disabled(!hasVPNEntitlement) } - - if settingsViewModel.state.subscription.entitlements.contains(.dataBrokerProtection) { + + if subscriptionFeatures.contains(.dataBrokerProtection) { + let hasDBPEntitlement = userEntitlements.contains(.dataBrokerProtection) + NavigationLink(destination: LazyView(SubscriptionPIRView()), isActive: $isShowingDBP) { SettingsCellView( label: UserText.settingsPProDBPTitle, image: Image("SettingsPrivacyProPIR"), - statusIndicator: StatusIndicatorView(status: .on) + statusIndicator: StatusIndicatorView(status: hasDBPEntitlement ? .on : .off), + isGreyedOut: !hasDBPEntitlement ) } + .disabled(!hasDBPEntitlement) } - - if settingsViewModel.state.subscription.entitlements.contains(.identityTheftRestoration) { - NavigationLink( - destination: LazyView(SubscriptionITPView()), - isActive: $isShowingITP) { - SettingsCellView( - label: UserText.settingsPProITRTitle, - image: Image("SettingsPrivacyProITP"), - statusIndicator: StatusIndicatorView(status: .on) - ) + + if subscriptionFeatures.contains(.identityTheftRestoration) || subscriptionFeatures.contains(.identityTheftRestorationGlobal) { + let hasITREntitlement = userEntitlements.contains(.identityTheftRestoration) || userEntitlements.contains(.identityTheftRestorationGlobal) + + NavigationLink(destination: LazyView(SubscriptionITPView()), isActive: $isShowingITP) { + SettingsCellView( + label: UserText.settingsPProITRTitle, + image: Image("SettingsPrivacyProITP"), + statusIndicator: StatusIndicatorView(status: hasITREntitlement ? .on : .off), + isGreyedOut: !hasITREntitlement + ) } + .disabled(!hasITREntitlement) } NavigationLink(destination: LazyView(SubscriptionSettingsView(configuration: .subscribed, settingsViewModel: settingsViewModel)) diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index 5f96ce0215..a97700ad72 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -46,7 +46,7 @@ final class SettingsViewModel: ObservableObject { let textZoomCoordinator: TextZoomCoordinating // Subscription Dependencies - private let subscriptionManager: SubscriptionManager + let subscriptionManager: SubscriptionManager let subscriptionFeatureAvailability: SubscriptionFeatureAvailability private var subscriptionSignOutObserver: Any? var duckPlayerContingencyHandler: DuckPlayerContingencyHandler { @@ -750,7 +750,7 @@ extension SettingsViewModel { // Check entitlements and update state var currentEntitlements: [Entitlement.ProductName] = [] - let entitlementsToCheck: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration] + let entitlementsToCheck: [Entitlement.ProductName] = [.networkProtection, .dataBrokerProtection, .identityTheftRestoration, .identityTheftRestorationGlobal] for entitlement in entitlementsToCheck { if case .success(true) = await subscriptionManager.accountManager.hasEntitlement(forProductName: entitlement) { @@ -759,6 +759,7 @@ extension SettingsViewModel { } self.state.subscription.entitlements = currentEntitlements + self.state.subscription.subscriptionFeatures = await subscriptionManager.currentSubscriptionFeatures() case .failure: break diff --git a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackFormViewModel.swift b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackFormViewModel.swift index 6ecfd2aa63..a7c6716fb6 100644 --- a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackFormViewModel.swift +++ b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackFormViewModel.swift @@ -19,6 +19,7 @@ import Combine import SwiftUI +import Subscription final class UnifiedFeedbackFormViewModel: ObservableObject { enum Source: String { @@ -104,16 +105,33 @@ final class UnifiedFeedbackFormViewModel: ObservableObject { let source: String + private(set) var availableCategories: [UnifiedFeedbackCategory] = [.subscription] + init(vpnMetadataCollector: any UnifiedMetadataCollector, defaultMetadatCollector: any UnifiedMetadataCollector = DefaultMetadataCollector(), feedbackSender: any UnifiedFeedbackSender = DefaultFeedbackSender(), - source: Source = .unknown) { + source: Source = .unknown, + subscriptionManager: any SubscriptionManager) { self.viewState = .feedbackPending self.vpnMetadataCollector = vpnMetadataCollector self.defaultMetadataCollector = defaultMetadatCollector self.feedbackSender = feedbackSender self.source = source.rawValue + + Task { + let features = await subscriptionManager.currentSubscriptionFeatures() + + if features.contains(.networkProtection) { + availableCategories.append(.vpn) + } + if features.contains(.dataBrokerProtection) { + availableCategories.append(.pir) + } + if features.contains(.identityTheftRestoration) || features.contains(.identityTheftRestorationGlobal) { + availableCategories.append(.itr) + } + } } @MainActor diff --git a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift index 58b43f15d7..8fc22088f3 100644 --- a/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift +++ b/DuckDuckGo/Subscription/Feedback/UnifiedFeedbackRootView.swift @@ -24,7 +24,7 @@ struct UnifiedFeedbackRootView: View { @StateObject var viewModel: UnifiedFeedbackFormViewModel var body: some View { - UnifiedFeedbackCategoryView(UserText.pproFeedbackFormTitle, sources: UnifiedFeedbackReportType.self, selection: $viewModel.selectedReportType) { + UnifiedFeedbackCategoryView(UserText.pproFeedbackFormTitle, options: UnifiedFeedbackReportType.allCases, selection: $viewModel.selectedReportType) { if let selectedReportType = viewModel.selectedReportType { switch UnifiedFeedbackReportType(rawValue: selectedReportType) { case nil: @@ -54,7 +54,7 @@ struct UnifiedFeedbackRootView: View { @ViewBuilder func reportProblemView() -> some View { UnifiedFeedbackCategoryView(UserText.pproFeedbackFormReportProblemTitle, - sources: UnifiedFeedbackCategory.self, + options: viewModel.availableCategories, selection: $viewModel.selectedCategory) { Group { if let selectedCategory = viewModel.selectedCategory { @@ -63,28 +63,28 @@ struct UnifiedFeedbackRootView: View { EmptyView() case .subscription: UnifiedFeedbackCategoryView(UserText.pproFeedbackFormReportPProProblemTitle, - sources: PrivacyProFeedbackSubcategory.self, + options: PrivacyProFeedbackSubcategory.allCases, selection: $viewModel.selectedSubcategory) { IssueDescriptionFormView(viewModel: viewModel, placeholder: UserText.pproFeedbackFormReportProblemPlaceholder) } case .vpn: UnifiedFeedbackCategoryView(UserText.pproFeedbackFormReportVPNProblemTitle, - sources: VPNFeedbackSubcategory.self, + options: VPNFeedbackSubcategory.allCases, selection: $viewModel.selectedSubcategory) { IssueDescriptionFormView(viewModel: viewModel, placeholder: UserText.pproFeedbackFormReportProblemPlaceholder) } case .pir: UnifiedFeedbackCategoryView(UserText.pproFeedbackFormReportPIRProblemTitle, - sources: PIRFeedbackSubcategory.self, + options: PIRFeedbackSubcategory.allCases, selection: $viewModel.selectedSubcategory) { IssueDescriptionFormView(viewModel: viewModel, placeholder: UserText.pproFeedbackFormReportProblemPlaceholder) } case .itr: UnifiedFeedbackCategoryView(UserText.pproFeedbackFormReportITRProblemTitle, - sources: ITRFeedbackSubcategory.self, + options: ITRFeedbackSubcategory.allCases, selection: $viewModel.selectedSubcategory) { IssueDescriptionFormView(viewModel: viewModel, placeholder: UserText.pproFeedbackFormReportProblemPlaceholder) @@ -106,21 +106,21 @@ struct UnifiedFeedbackRootView: View { } } -struct UnifiedFeedbackCategoryView: View where Category.AllCases == [Category], Category.RawValue == String { +struct UnifiedFeedbackCategoryView: View where Category.RawValue == String { let title: String let prompt: String - let sources: Category.Type + let options: [Category] let selection: Binding let destination: () -> Destination init(_ title: String, prompt: String = UserText.pproFeedbackFormSelectCategoryTitle, - sources: Category.Type, + options: [Category], selection: Binding, @ViewBuilder destination: @escaping () -> Destination) { self.title = title self.prompt = prompt - self.sources = sources + self.options = options self.selection = selection self.destination = destination } @@ -129,7 +129,7 @@ struct UnifiedFeedbackCategoryView Void)? var onBackToSettings: (() -> Void)? - var onFeatureSelected: ((SubscriptionFeatureSelection) -> Void)? + var onFeatureSelected: ((Entitlement.ProductName) -> Void)? var onActivateSubscription: (() -> Void)? struct FeatureSelection: Codable { - let feature: String + let productFeature: Entitlement.ProductName } weak var broker: UserScriptMessageBroker? @@ -206,7 +206,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec if subscriptionFeatureAvailability.isSubscriptionPurchaseAllowed { return subscriptionOptions } else { - return SubscriptionOptions.empty + return subscriptionOptions.withoutPurchaseOptions() } } else { Logger.subscription.error("Failed to obtain subscription options") @@ -330,15 +330,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec return nil } - guard let featureSelection = SubscriptionFeatureSelection(featureName: featureSelection.feature) else { - assertionFailure("SubscriptionPagesUserScript: unexpected feature name value") - Logger.subscription.error("SubscriptionPagesUserScript: unexpected feature name value") - setTransactionError(.generalError) - return nil - } + onFeatureSelected?(featureSelection.productFeature) - onFeatureSelected?(featureSelection) - return nil } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift index 75b49918cb..a059bcae64 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionEmailViewModel.swift @@ -170,15 +170,17 @@ final class SubscriptionEmailViewModel: ObservableObject { subFeature.onFeatureSelected = { feature in DispatchQueue.main.async { switch feature { - case .netP: + case .networkProtection: UniquePixel.fire(pixel: .privacyProWelcomeVPN) self.state.selectedFeature = .netP - case .itr: + case .dataBrokerProtection: UniquePixel.fire(pixel: .privacyProWelcomePersonalInformationRemoval) self.state.selectedFeature = .itr - case .dbp: + case .identityTheftRestoration, .identityTheftRestorationGlobal: UniquePixel.fire(pixel: .privacyProWelcomeIdentityRestoration) self.state.selectedFeature = .dbp + case .unknown: + break } } diff --git a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift index 04a59ff21c..5dc11256ab 100644 --- a/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift +++ b/DuckDuckGo/Subscription/ViewModel/SubscriptionFlowViewModel.swift @@ -115,15 +115,17 @@ final class SubscriptionFlowViewModel: ObservableObject { subFeature.onFeatureSelected = { feature in DispatchQueue.main.async { switch feature { - case .netP: + case .networkProtection: UniquePixel.fire(pixel: .privacyProWelcomeVPN) self.state.selectedFeature = .netP - case .dbp: + case .dataBrokerProtection: UniquePixel.fire(pixel: .privacyProWelcomePersonalInformationRemoval) self.state.selectedFeature = .dbp - case .itr: + case .identityTheftRestoration, .identityTheftRestorationGlobal: UniquePixel.fire(pixel: .privacyProWelcomeIdentityRestoration) self.state.selectedFeature = .itr + case .unknown: + break } } } diff --git a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift index 82c7b3f04b..5d6f5c57d8 100644 --- a/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift +++ b/DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift @@ -243,7 +243,9 @@ struct SubscriptionSettingsView: View { @ViewBuilder private var supportButton: some View { - let viewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), source: .ppro) + let viewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: DefaultVPNMetadataCollector(), + source: .ppro, + subscriptionManager: settingsViewModel.subscriptionManager) NavigationLink(UserText.subscriptionFeedback, destination: UnifiedFeedbackRootView(viewModel: viewModel)) .daxBodyRegular() .foregroundColor(.init(designSystemColor: .textPrimary)) diff --git a/DuckDuckGo/SubscriptionDebugViewController.swift b/DuckDuckGo/SubscriptionDebugViewController.swift index 5bd7505d64..6dbc713030 100644 --- a/DuckDuckGo/SubscriptionDebugViewController.swift +++ b/DuckDuckGo/SubscriptionDebugViewController.swift @@ -23,6 +23,7 @@ import Subscription import Core import NetworkProtection import StoreKit +import BrowserServicesKit final class SubscriptionDebugViewController: UITableViewController { @@ -30,6 +31,9 @@ final class SubscriptionDebugViewController: UITableViewController { private var subscriptionManager: SubscriptionManager { AppDependencyProvider.shared.subscriptionManager } + private var featureFlagger: FeatureFlagger { + AppDependencyProvider.shared.featureFlagger + } // swiftlint:disable:next force_cast private let reporter = (UIApplication.shared.delegate as! AppDelegate).privacyProDataReporter as! PrivacyProDataReporter @@ -40,7 +44,8 @@ final class SubscriptionDebugViewController: UITableViewController { Sections.appstore: "App Store", Sections.environment: "Environment", Sections.pixels: "Promo Pixel Parameters", - Sections.metadata: "StoreKit Metadata" + Sections.metadata: "StoreKit Metadata", + Sections.featureFlags: "Feature flags" ] enum Sections: Int, CaseIterable { @@ -50,6 +55,7 @@ final class SubscriptionDebugViewController: UITableViewController { case environment case pixels case metadata + case featureFlags } enum AuthorizationRows: Int, CaseIterable { @@ -82,6 +88,10 @@ final class SubscriptionDebugViewController: UITableViewController { case countryCode } + enum FeatureFlagRows: Int, CaseIterable { + case isLaunchedROW + } + private var storefrontID = "Loading" private var storefrontCountryCode = "Loading" @@ -173,6 +183,16 @@ final class SubscriptionDebugViewController: UITableViewController { case .none: break } + + case .featureFlags: + switch FeatureFlagRows(rawValue: indexPath.row) { + case .isLaunchedROW: + cell.textLabel?.text = "isPrivacyProLaunchedROWOverride" + cell.accessoryType = featureFlagger.isFeatureOn(.isPrivacyProLaunchedROWOverride) ? .checkmark : .none + case .none: + break + } + case .none: break } @@ -188,6 +208,7 @@ final class SubscriptionDebugViewController: UITableViewController { case .environment: return EnvironmentRows.allCases.count case .pixels: return PixelsRows.allCases.count case .metadata: return MetadataRows.allCases.count + case .featureFlags: return FeatureFlagRows.allCases.count case .none: return 0 } } @@ -223,6 +244,11 @@ final class SubscriptionDebugViewController: UITableViewController { } case .metadata: break + case .featureFlags: + switch FeatureFlagRows(rawValue: indexPath.row) { + case .isLaunchedROW: toggleIsLaunchedROWFlag() + default: break + } case .none: break } @@ -331,6 +357,16 @@ final class SubscriptionDebugViewController: UITableViewController { showAlert(title: "", message: message) } + private func toggleIsLaunchedROWFlag() { + let flag = FeatureFlag.isPrivacyProLaunchedROWOverride + if featureFlagger.localOverrides?.override(for: flag) == nil { + featureFlagger.localOverrides?.toggleOverride(for: flag) + } else { + featureFlagger.localOverrides?.clearOverride(for: flag) + } + tableView.reloadData() + } + private func syncAppleIDAccount() { Task { do { diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index b601eb9f28..b6bb75c759 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1084,6 +1084,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let settingsPProSectionFooter = NSLocalizedString("settings.ppro.footer", value: "Privacy Policy and Terms of Service", comment: "Title for Link in the Footer of Privacy Pro section") public static let settingsPProSubscribe = NSLocalizedString("settings.subscription.subscribe", value: "Protect your connection and identity with Privacy Pro", comment: "Call to action title for Privacy Pro settings") public static let settingsPProDescription = NSLocalizedString("settings.subscription.description", value:"Includes our VPN, Personal Information Removal, and Identity Theft Restoration.", comment: "Privacy pro description subtitle in settings") + public static let settingsPProROWDescription = NSLocalizedString("settings.subscription.row.description", value:"Includes our VPN and Identity Theft Restoration.", comment: "Privacy Pro description subtitle in settings") public static let settingsPProActivating = NSLocalizedString("settings.subscription.activating", value:"Activating", comment: "Privacy pro description subtitle in settings when the is activating") public static let settingsPProLearnMore = NSLocalizedString("settings.subscription.learn.more", value: "Get Privacy Pro", comment: "Get Privacy Pro button text for privacy pro") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index fc7d856f4a..ca98d2afcc 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -2370,6 +2370,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Subscription Settings button text for privacy pro */ "settings.subscription.manage" = "Subscription Settings"; +/* Privacy Pro description subtitle in settings */ +"settings.subscription.row.description" = "Includes our VPN and Identity Theft Restoration."; + /* Call to action title for Privacy Pro settings */ "settings.subscription.subscribe" = "Protect your connection and identity with Privacy Pro"; diff --git a/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift b/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift index 47428ed53d..de2c62cac0 100644 --- a/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift +++ b/DuckDuckGoTests/NetworkProtectionStatusViewModelTests.swift @@ -22,12 +22,14 @@ import NetworkProtection import NetworkExtension import NetworkProtectionTestUtils import SubscriptionTestingUtilities +import Subscription @testable import DuckDuckGo final class NetworkProtectionStatusViewModelTests: XCTestCase { private var tunnelController: MockTunnelController! private var statusObserver: MockConnectionStatusObserver! private var serverInfoObserver: MockConnectionServerInfoObserver! + private var subscriptionManager: SubscriptionManagerMock! private var viewModel: NetworkProtectionStatusViewModel! private var testError: Error { @@ -40,12 +42,20 @@ final class NetworkProtectionStatusViewModelTests: XCTestCase { tunnelController = MockTunnelController() statusObserver = MockConnectionStatusObserver() serverInfoObserver = MockConnectionServerInfoObserver() + subscriptionManager = SubscriptionManagerMock(accountManager: AccountManagerMock(), + subscriptionEndpointService: SubscriptionEndpointServiceMock(), + authEndpointService: AuthEndpointServiceMock(), + storePurchaseManager: StorePurchaseManagerMock(), + currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore), + canPurchase: true, + subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock()) viewModel = NetworkProtectionStatusViewModel(tunnelController: tunnelController, settings: VPNSettings(defaults: .networkProtectionGroupDefaults), statusObserver: statusObserver, serverInfoObserver: serverInfoObserver, locationListRepository: MockNetworkProtectionLocationListRepository(), - usesUnifiedFeedbackForm: false) + usesUnifiedFeedbackForm: false, + subscriptionManager: subscriptionManager) } override func tearDown() { diff --git a/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift b/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift index d8f38e823f..c3004d38fb 100644 --- a/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift +++ b/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift @@ -41,7 +41,8 @@ final class StorePurchaseManagerTests: XCTestCase { session.disableDialogs = true session.clearTransactions() - storePurchaseManager = DefaultStorePurchaseManager() + let subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() + storePurchaseManager = DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) } override func tearDownWithError() throws { @@ -70,7 +71,7 @@ final class StorePurchaseManagerTests: XCTestCase { // Then XCTAssertEqual(subscriptionOptions.options.count, 2) - XCTAssertEqual(subscriptionOptions.features.count, SubscriptionFeatureName.allCases.count) + XCTAssertEqual(subscriptionOptions.features.count, 3) XCTAssertTrue(storePurchaseManager.areProductsAvailable) let optionIDs = subscriptionOptions.options.map { $0.id } diff --git a/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift b/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift index 2d4979997f..7efea415d4 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionContainerViewModelTests.swift @@ -30,13 +30,15 @@ final class SubscriptionContainerViewModelTests: XCTestCase { let subscriptionService = SubscriptionEndpointServiceMock() let authService = AuthEndpointServiceMock() let storePurchaseManager = StorePurchaseManagerMock() + let subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() return SubscriptionManagerMock(accountManager: accountManager, subscriptionEndpointService: subscriptionService, authEndpointService: authService, storePurchaseManager: storePurchaseManager, currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore), - canPurchase: true) + canPurchase: true, + subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) }() let subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock.enabled diff --git a/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift b/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift index 67235b541e..cc114e794b 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionFlowViewModelTests.swift @@ -29,14 +29,16 @@ final class SubscriptionFlowViewModelTests: XCTestCase { let accountManager = AccountManagerMock() let subscriptionService = DefaultSubscriptionEndpointService(currentServiceEnvironment: .production) let authService = DefaultAuthEndpointService(currentServiceEnvironment: .production) - let storePurchaseManager = DefaultStorePurchaseManager() + let subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() + let storePurchaseManager = DefaultStorePurchaseManager(subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) return SubscriptionManagerMock(accountManager: accountManager, subscriptionEndpointService: subscriptionService, authEndpointService: authService, storePurchaseManager: storePurchaseManager, currentEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore), - canPurchase: true) + canPurchase: true, + subscriptionFeatureMappingCache: subscriptionFeatureMappingCache) }() let subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock.enabled diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index 8636921470..0eb8d432a6 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -46,7 +46,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { static let mostRecentTransactionJWS = "dGhpcyBpcyBub3QgYSByZWFsIEFw(...)cCBTdG9yZSB0cmFuc2FjdGlvbiBKV1M=" - static let subscriptionOptions = SubscriptionOptions(platform: SubscriptionPlatformName.ios.rawValue, + static let subscriptionOptions = SubscriptionOptions(platform: SubscriptionPlatformName.ios, options: [ SubscriptionOption(id: "1", cost: SubscriptionOptionCost(displayPrice: "9 USD", recurrence: "monthly")), @@ -54,9 +54,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { cost: SubscriptionOptionCost(displayPrice: "99 USD", recurrence: "yearly")) ], features: [ - SubscriptionFeature(name: "vpn"), - SubscriptionFeature(name: "personal-information-removal"), - SubscriptionFeature(name: "identity-theft-restoration") + SubscriptionFeature(name: .networkProtection), + SubscriptionFeature(name: .dataBrokerProtection), + SubscriptionFeature(name: .identityTheftRestoration) ]) static let validateTokenResponse = ValidateTokenResponse(account: ValidateTokenResponse.Account(email: Constants.email, @@ -81,6 +81,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { var storePurchaseManager: StorePurchaseManagerMock! var subscriptionEnvironment: SubscriptionEnvironment! + var subscriptionFeatureMappingCache: SubscriptionFeatureMappingCacheMock! + var subscriptionFeatureFlagger: FeatureFlaggerMapping! + var appStorePurchaseFlow: AppStorePurchaseFlow! var appStoreRestoreFlow: AppStoreRestoreFlow! var appStoreAccountManagementFlow: AppStoreAccountManagementFlow! @@ -129,6 +132,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { key: UserDefaultsCacheKey.subscriptionEntitlements, settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + subscriptionFeatureMappingCache = SubscriptionFeatureMappingCacheMock() + subscriptionFeatureFlagger = FeatureFlaggerMapping(mapping: { $0.defaultState }) + // Real AccountManager accountManager = DefaultAccountManager(storage: accountStorage, accessTokenStorage: accessTokenStorage, @@ -156,7 +162,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { accountManager: accountManager, subscriptionEndpointService: subscriptionService, authEndpointService: authService, - subscriptionEnvironment: subscriptionEnvironment) + subscriptionFeatureMappingCache: subscriptionFeatureMappingCache, + subscriptionEnvironment: subscriptionEnvironment, + subscriptionFeatureFlagger: subscriptionFeatureFlagger) feature = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, subscriptionFeatureAvailability: subscriptionFeatureAvailability, @@ -812,11 +820,11 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let onFeatureSelectedCalled = expectation(description: "onFeatureSelected") feature.onFeatureSelected = { selection in onFeatureSelectedCalled.fulfill() - XCTAssertEqual(selection, SubscriptionFeatureSelection.itr) + XCTAssertEqual(selection, .identityTheftRestoration) } // When - let featureSelectionParams = ["feature": SubscriptionFeatureName.itr] + let featureSelectionParams = ["productFeature": Entitlement.ProductName.identityTheftRestoration.rawValue] let result = await feature.featureSelected(params: featureSelectionParams, original: Constants.mockScriptMessage) // Then From 68a54ccb57be24eab988890f519f048ed4708858 Mon Sep 17 00:00:00 2001 From: Bartek Waresiak Date: Fri, 29 Nov 2024 13:34:48 +0100 Subject: [PATCH 07/12] Revert incorrect push --- DuckDuckGo.xcodeproj/project.pbxproj | 4 + DuckDuckGoTests/PixelTests.swift | 4 +- DuckDuckGoTests/SpecialErrorPageTests.swift | 304 ++++++++++++++++++++ 3 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 DuckDuckGoTests/SpecialErrorPageTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6b1fa683d5..3273c57da7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -982,6 +982,7 @@ CBAA195A27BFE15600A4BD49 /* NSManagedObjectContextExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */; }; CBC83E3429B631780008E19C /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = CBC83E3329B631780008E19C /* Configuration */; }; CBC88EE12C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */; }; + CBC88EE32C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE22C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift */; }; CBC88EE52C8097B500F0F8C5 /* URLCredentialCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */; }; CBCCF96828885DEE006F4A71 /* AppPrivacyConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C4BC3127C3F9B600C40026 /* AppPrivacyConfigurationTests.swift */; }; CBD4F13C279EBF4A00B20FD7 /* HomeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */; }; @@ -2816,6 +2817,7 @@ CBB6B2542AF6D543006B777C /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = ""; }; CBC7AB542AF6D583008CB798 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScriptTests.swift; sourceTree = ""; }; + CBC88EE22C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageTests.swift; sourceTree = ""; }; CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLCredentialCreator.swift; sourceTree = ""; }; CBC8DC252AF6D4CD00BA681A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessage.swift; sourceTree = ""; }; @@ -5999,6 +6001,7 @@ F189AED61F18F6DE001EBAE1 /* TabTests.swift */, D625AAEA2BBEEFC900BC189A /* TabURLInterceptorTests.swift */, CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */, + CBC88EE22C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift */, ); name = Tabs; sourceTree = ""; @@ -8154,6 +8157,7 @@ CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */, C158AC7B297AB5DC0008723A /* MockSecureVault.swift in Sources */, 569437342BE4E41500C0881B /* SyncErrorHandlerSyncErrorsAlertsTests.swift in Sources */, + CBC88EE32C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift in Sources */, 85C11E4120904BBE00BFFEB4 /* VariantManagerTests.swift in Sources */, F1134ECE1F40EA9C00B73467 /* AtbParserTests.swift in Sources */, F189AEE41F18FDAF001EBAE1 /* LinkTests.swift in Sources */, diff --git a/DuckDuckGoTests/PixelTests.swift b/DuckDuckGoTests/PixelTests.swift index 2d4ee61a94..132517dac6 100644 --- a/DuckDuckGoTests/PixelTests.swift +++ b/DuckDuckGoTests/PixelTests.swift @@ -30,6 +30,7 @@ class PixelTests: XCTestCase { let userAgentName = "User-Agent" override func setUpWithError() throws { + throw XCTSkip("Potentially flaky") try super.setUpWithError() Pixel.isDryRun = false @@ -161,8 +162,7 @@ class PixelTests: XCTestCase { wait(for: [expectation], timeout: 5.0) } - func testPixelDebouncePreventsFiringWithinInterval() throws { - throw XCTSkip("Flaky") + func testPixelDebouncePreventsFiringWithinInterval() { let firstFireExpectation = XCTestExpectation(description: "First pixel fire should succeed") let thirdFireExpectation = XCTestExpectation(description: "Third pixel fire should succeed after debounce interval") diff --git a/DuckDuckGoTests/SpecialErrorPageTests.swift b/DuckDuckGoTests/SpecialErrorPageTests.swift new file mode 100644 index 0000000000..f3ff737d3b --- /dev/null +++ b/DuckDuckGoTests/SpecialErrorPageTests.swift @@ -0,0 +1,304 @@ +// +// SpecialErrorPageTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +import WebKit + +@testable import SpecialErrorPages +@testable import DuckDuckGo + +class MockSpecialErrorWebView: WKWebView { + + var loadRequestHandler: ((URLRequest, String) -> Void)? + var currentURL: URL? + + override func loadSimulatedRequest(_ request: URLRequest, responseHTML string: String) -> WKNavigation { + loadRequestHandler?(request, string) + return super.loadSimulatedRequest(request, responseHTML: string) + } + + override var url: URL? { + return currentURL + } + + func setCurrentURL(_ url: URL) { + self.currentURL = url + } + +} + +final class SpecialErrorPageTests: XCTestCase { + + var webView: MockSpecialErrorWebView! + var sut: TabViewController! + + override func setUpWithError() throws { + throw XCTSkip("Potentially Flaky") + + try super.setUpWithError() + let featureFlagger = MockFeatureFlagger() + featureFlagger.enabledFeatureFlags = [.sslCertificatesBypass] + sut = .fake(customWebView: { [weak self] configuration in + guard let self else { fatalError("It has to exist") } + self.webView = MockSpecialErrorWebView(frame: CGRect(), configuration: configuration) + return self.webView + }, featureFlagger: featureFlagger) + WKNavigation.swizzleDealloc() + } + + override func tearDown() async throws { + try await super.tearDown() + WKNavigation.restoreDealloc() + } + + func testWhenCertificateExpiredThenExpectedErrorPageIsShown() { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLCertExpired, + NSURLErrorFailingURLErrorKey: URL(string: "https://expired.badssl.com")!]) + let expectation = self.expectation(description: "Special error page should be loaded") + var didFulfill = false + webView.loadRequestHandler = { request, html in + if !didFulfill { + XCTAssertTrue(html.contains("Warning: This site may be insecure")) + XCTAssertTrue(html.contains("is expired")) + XCTAssertEqual(request.url!.host, URL(string: "https://expired.badssl.com")!.host) + expectation.fulfill() + didFulfill = true + } + } + + // WHEN + sut.webView(webView, didFailProvisionalNavigation: WKNavigation(), withError: error) + + // THEN + XCTAssertEqual(sut.failedURL, URL(string: "https://expired.badssl.com")!) + XCTAssertEqual(sut.errorData, SpecialErrorData(kind: .ssl, + errorType: "expired", + domain: "expired.badssl.com", + eTldPlus1: "badssl.com")) + waitForExpectations(timeout: 1) { error in + XCTAssertNil(error, "Expectation was not fulfilled in time") + } + } + + func testWhenCertificateWrongHostThenExpectedErrorPageIsShown() { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLHostNameMismatch, + NSURLErrorFailingURLErrorKey: URL(string: "https://wrong.host.badssl.com")!]) + let expectation = self.expectation(description: "Special error page should be loaded") + var didFulfill = false + webView.loadRequestHandler = { request, html in + if !didFulfill { + XCTAssertTrue(html.contains("Warning: This site may be insecure")) + XCTAssertTrue(html.contains("does not match")) + XCTAssertEqual(request.url!.host, URL(string: "https://wrong.host.badssl.com")!.host) + expectation.fulfill() + didFulfill = true + } + } + + // WHEN + sut.webView(webView, didFailProvisionalNavigation: WKNavigation(), withError: error) + + // THEN + XCTAssertEqual(sut.failedURL, URL(string: "https://wrong.host.badssl.com")!) + XCTAssertEqual(sut.errorData, SpecialErrorData(kind: .ssl, + errorType: "wrongHost", + domain: "wrong.host.badssl.com", + eTldPlus1: "badssl.com")) + waitForExpectations(timeout: 1) { error in + XCTAssertNil(error, "Expectation was not fulfilled in time") + } + } + + func testWhenCertificateSelfSignedThenExpectedErrorPageIsShown() { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLXCertChainInvalid, + NSURLErrorFailingURLErrorKey: URL(string: "https://self-signed.badssl.com")!]) + let expectation = self.expectation(description: "Special error page should be loaded") + var didFulfill = false + webView.loadRequestHandler = { request, html in + if !didFulfill { + XCTAssertTrue(html.contains("Warning: This site may be insecure")) + XCTAssertTrue(html.contains("is not trusted")) + XCTAssertEqual(request.url!.host, URL(string: "https://self-signed.badssl.com")!.host) + expectation.fulfill() + didFulfill = true + } + } + + // WHEN + sut.webView(webView, didFailProvisionalNavigation: WKNavigation(), withError: error) + + // THEN + XCTAssertEqual(sut.failedURL, URL(string: "https://self-signed.badssl.com")!) + XCTAssertEqual(sut.errorData, SpecialErrorData(kind: .ssl, + errorType: "selfSigned", + domain: "self-signed.badssl.com", + eTldPlus1: "badssl.com")) + waitForExpectations(timeout: 1) { error in + XCTAssertNil(error, "Expectation was not fulfilled in time") + } + } + + func testWhenOtherCertificateIssueThenExpectedErrorPageIsShown() { + // GIVEN + let error = NSError(domain: "test", + code: NSURLErrorServerCertificateUntrusted, + userInfo: ["_kCFStreamErrorCodeKey": errSSLUnknownRootCert, + NSURLErrorFailingURLErrorKey: URL(string: "https://untrusted-root.badssl.com")!]) + let expectation = self.expectation(description: "Special error page should be loaded") + var didFulfill = false + webView.loadRequestHandler = { request, html in + if !didFulfill { + XCTAssertTrue(html.contains("Warning: This site may be insecure")) + XCTAssertTrue(html.contains("is not trusted")) + XCTAssertEqual(request.url!.host, URL(string: "https://untrusted-root.badssl.com")!.host) + expectation.fulfill() + didFulfill = true + } + } + + // WHEN + sut.webView(webView, didFailProvisionalNavigation: WKNavigation(), withError: error) + + // THEN + XCTAssertEqual(sut.failedURL, URL(string: "https://untrusted-root.badssl.com")!) + XCTAssertEqual(sut.errorData, SpecialErrorData(kind: .ssl, + errorType: "invalid", + domain: "untrusted-root.badssl.com", + eTldPlus1: "badssl.com")) + waitForExpectations(timeout: 1) { error in + XCTAssertNil(error, "Expectation was not fulfilled in time") + } + } + + @MainActor + func testWhenNavigationEndedIfNoSSLFailureSSLUserScriptIsNotEnabled() { + // GIVEN + webView.setCurrentURL(URL(string: "https://self-signed.badssl.com")!) + sut.storedSpecialErrorPageUserScript = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") + + // WHEN + sut.webView(webView, didFinish: WKNavigation()) + + // THEN + XCTAssertFalse(sut.specialErrorPageUserScript?.isEnabled ?? true) + } + + @MainActor + func testWhenNavigationEndedIfSSLFailureButURLIsDifferentFromNavigationURLThenSSLUserScriptIsNotEnabled() { + // GIVEN + webView.setCurrentURL(URL(string: "https://self-signed.badssl.com")!) + sut.failedURL = URL(string: "https://different.url.com")! + sut.storedSpecialErrorPageUserScript = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") + + // WHEN + sut.webView(webView, didFinish: WKNavigation()) + + // THEN + XCTAssertFalse(sut.specialErrorPageUserScript?.isEnabled ?? true) + } + + @MainActor + func testWhenNavigationEndedIfSSLFailureAndNavigationURLIsTheSameAsFailingURLThenSSLUserScriptIsEnabled() { + // GIVEN + webView.setCurrentURL(URL(string: "https://self-signed.badssl.com")!) + sut.failedURL = URL(string: "https://self-signed.badssl.com")! + sut.storedSpecialErrorPageUserScript = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") + + // WHEN + sut.webView(webView, didFinish: WKNavigation()) + + // THEN + XCTAssertTrue(sut.specialErrorPageUserScript?.isEnabled ?? false) + } + + func testWhenDidReceiveChallengeIfChallengeForCertificateValidationAndNoBypassThenShouldNotReturnCredentials() async { + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallengeSender()) + await sut.webView(webView, didReceive: challenge) { _, credential in + XCTAssertNil(credential) + } + } + + func testWhenDidReceiveChallengeIfChallengeForCertificateValidationAndUserRequestBypassThenReturnsCredentials() async { + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallengeSender()) + await sut.visitSite() + await sut.webView(webView, didReceive: challenge) { _, credential in + XCTAssertNotNil(credential) + } + } + +} + +final class ChallengeSender: URLAuthenticationChallengeSender { + func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {} + func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {} + func cancel(_ challenge: URLAuthenticationChallenge) {} + func isEqual(_ object: Any?) -> Bool { + return false + } + var hash: Int = 0 + var superclass: AnyClass? + func `self`() -> Self { + self + } + func perform(_ aSelector: Selector!) -> Unmanaged! { + return nil + } + func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged! { + return nil + } + func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged! { + return nil + } + func isProxy() -> Bool { + return false + } + func isKind(of aClass: AnyClass) -> Bool { + return false + } + func isMember(of aClass: AnyClass) -> Bool { + return false + } + func conforms(to aProtocol: Protocol) -> Bool { + return false + } + func responds(to aSelector: Selector!) -> Bool { + return false + } + var description: String = "" +} + +final class MockCredentialCreator: URLCredentialCreating { + + func urlCredentialFrom(trust: SecTrust?) -> URLCredential? { + return URLCredential(user: "", password: "", persistence: .forSession) + } + +} From c28b0022d7dd4b5e5d6500fdf009aeda5c20584d Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 29 Nov 2024 13:42:00 +0100 Subject: [PATCH 08/12] Release 7.147.0-6 (#3652) Please make sure all GH checks passed before merging. It can take around 20 minutes. Briefly review this PR to see if there are no issues or red flags and then merge it. --- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0250667290..f4c3a54c31 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9271,7 +9271,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9308,7 +9308,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9398,7 +9398,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9425,7 +9425,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9572,7 +9572,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9597,7 +9597,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9666,7 +9666,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9700,7 +9700,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9733,7 +9733,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9763,7 +9763,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10073,7 +10073,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10104,7 +10104,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10132,7 +10132,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10165,7 +10165,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10195,7 +10195,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10228,11 +10228,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 5; + DYLIB_CURRENT_VERSION = 6; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10464,7 +10464,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10492,7 +10492,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10524,7 +10524,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10561,7 +10561,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10596,7 +10596,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10631,11 +10631,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 5; + DYLIB_CURRENT_VERSION = 6; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10808,11 +10808,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 5; + DYLIB_CURRENT_VERSION = 6; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10841,10 +10841,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 5; + DYLIB_CURRENT_VERSION = 6; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; From 68a69a905b0816b551a320a299f6aa355a27e435 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:31:35 +0100 Subject: [PATCH 09/12] point to BSK branch and updates (#3636) Task/Issue URL: https://app.asana.com/0/1204186595873227/1208687542689225/f Tech Design URL: https://app.asana.com/0/1204186595873227/1208823859841917/f CC: **Description**: Updates BSK dependency --- Core/FeatureFlag.swift | 8 ++++---- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- DuckDuckGo/AppDependencyProvider.swift | 3 ++- DuckDuckGoTests/AppUserDefaultsTests.swift | 2 +- DuckDuckGoTests/DuckPlayerMocks.swift | 12 ++++++++++++ DuckDuckGoTests/MockFeatureFlagger.swift | 12 ++++++++++++ DuckDuckGoTests/MockPrivacyConfiguration.swift | 12 ++++++++++++ .../NewTabPageShortcutsSettingsModelTests.swift | 12 ++++++++++++ .../PrivacyConfigurationManagerMock.swift | 12 ++++++++++++ 10 files changed, 72 insertions(+), 11 deletions(-) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index da04bcfaf6..e500d934b4 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -66,7 +66,7 @@ extension FeatureFlag: FeatureFlagDescribing { public var source: FeatureFlagSource { switch self { case .debugMenu: - return .internalOnly + return .internalOnly() case .sync: return .remoteReleasable(.subfeature(SyncSubfeature.level0ShowSync)) case .autofillCredentialInjecting: @@ -106,9 +106,9 @@ extension FeatureFlag: FeatureFlagDescribing { case .syncPromotionPasswords: return .remoteReleasable(.subfeature(SyncPromotionSubfeature.passwords)) case .onboardingHighlights: - return .internalOnly + return .internalOnly() case .onboardingAddToDock: - return .internalOnly + return .internalOnly() case .autofillSurveys: return .remoteReleasable(.feature(.autofillSurveys)) case .autcompleteTabs: @@ -122,7 +122,7 @@ extension FeatureFlag: FeatureFlagDescribing { case .adAttributionReporting: return .remoteReleasable(.feature(.adAttributionReporting)) case .crashReportOptInStatusResetting: - return .internalOnly + return .internalOnly() } } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3273c57da7..a5f1b08056 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -11251,7 +11251,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 212.1.1; + version = 213.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4b616ab2cf..926b926006 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" : "db9c29a429896138fab29da987981a5f4a8d6712", - "version" : "212.1.1" + "revision" : "9563e1ea53ff0590e3bb78e566234907edb73678", + "version" : "213.0.0" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "757bbbae1e2afbb421caee9bfca04ee5c56c3af8", - "version" : "7.2.0" + "revision" : "49db79829dcb166b3524afdbc1c680890452ce1c", + "version" : "7.2.1" } }, { diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 245901da44..7e38af6475 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -94,7 +94,8 @@ final class AppDependencyProvider: DependencyProvider { private init() { featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, - privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager) + privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, + experimentManager: ExperimentCohortsManager(store: ExperimentsDataStore())) configurationManager = ConfigurationManager(store: configurationStore) diff --git a/DuckDuckGoTests/AppUserDefaultsTests.swift b/DuckDuckGoTests/AppUserDefaultsTests.swift index cc12ab3ecb..4aeb208c0b 100644 --- a/DuckDuckGoTests/AppUserDefaultsTests.swift +++ b/DuckDuckGoTests/AppUserDefaultsTests.swift @@ -226,7 +226,7 @@ class AppUserDefaultsTests: XCTestCase { mockManager.privacyConfig = mockConfiguration(subfeatureEnabled: enabled) let internalUserDecider = DefaultInternalUserDecider(store: internalUserDeciderStore) - return DefaultFeatureFlagger(internalUserDecider: internalUserDecider, privacyConfigManager: mockManager) + return DefaultFeatureFlagger(internalUserDecider: internalUserDecider, privacyConfigManager: mockManager, experimentManager: nil) } private func mockConfiguration(subfeatureEnabled: Bool) -> PrivacyConfiguration { diff --git a/DuckDuckGoTests/DuckPlayerMocks.swift b/DuckDuckGoTests/DuckPlayerMocks.swift index 7806c9d874..0c0844ebcd 100644 --- a/DuckDuckGoTests/DuckPlayerMocks.swift +++ b/DuckDuckGoTests/DuckPlayerMocks.swift @@ -254,6 +254,18 @@ final class MockDuckPlayerFeatureFlagger: FeatureFlagger { func isFeatureOn(for featureFlag: Flag, allowOverride: Bool) -> Bool { return !enabledFeatures.isEmpty } + + func getCohortIfEnabled(_ subfeature: any PrivacySubfeature) -> CohortID? { + return nil + } + + func getCohortIfEnabled(for featureFlag: Flag) -> (any FlagCohort)? where Flag: FeatureFlagExperimentDescribing { + return nil + } + + func getAllActiveExperiments() -> Experiments { + return [:] + } } final class MockDuckPlayerStorage: DuckPlayerStorage { diff --git a/DuckDuckGoTests/MockFeatureFlagger.swift b/DuckDuckGoTests/MockFeatureFlagger.swift index 7a8f568486..7f6d1a8866 100644 --- a/DuckDuckGoTests/MockFeatureFlagger.swift +++ b/DuckDuckGoTests/MockFeatureFlagger.swift @@ -39,4 +39,16 @@ final class MockFeatureFlagger: FeatureFlagger { } return true } + + func getCohortIfEnabled(_ subfeature: any PrivacySubfeature) -> CohortID? { + return nil + } + + func getCohortIfEnabled(for featureFlag: Flag) -> (any FlagCohort)? where Flag: FeatureFlagExperimentDescribing { + return nil + } + + func getAllActiveExperiments() -> Experiments { + return [:] + } } diff --git a/DuckDuckGoTests/MockPrivacyConfiguration.swift b/DuckDuckGoTests/MockPrivacyConfiguration.swift index 3066b1c3a6..07c69f3679 100644 --- a/DuckDuckGoTests/MockPrivacyConfiguration.swift +++ b/DuckDuckGoTests/MockPrivacyConfiguration.swift @@ -35,6 +35,18 @@ class MockPrivacyConfiguration: PrivacyConfiguration { return .disabled(.disabledInConfig) } + func stateFor(subfeatureID: SubfeatureID, parentFeatureID: ParentFeatureID, versionProvider: AppVersionProvider, randomizer: (Range) -> Double) -> PrivacyConfigurationFeatureState { + return .disabled(.disabledInConfig) + } + + func cohorts(for subfeature: any PrivacySubfeature) -> [PrivacyConfigurationData.Cohort]? { + return nil + } + + func cohorts(subfeatureID: SubfeatureID, parentFeatureID: ParentFeatureID) -> [PrivacyConfigurationData.Cohort]? { + return nil + } + var identifier: String = "MockPrivacyConfiguration" var version: String? = "123456789" var userUnprotectedDomains: [String] = [] diff --git a/DuckDuckGoTests/NewTabPageShortcutsSettingsModelTests.swift b/DuckDuckGoTests/NewTabPageShortcutsSettingsModelTests.swift index 9f3affb257..7561f7c206 100644 --- a/DuckDuckGoTests/NewTabPageShortcutsSettingsModelTests.swift +++ b/DuckDuckGoTests/NewTabPageShortcutsSettingsModelTests.swift @@ -73,4 +73,16 @@ private final class AlwaysTrueFeatureFlagger: FeatureFlagger { func isFeatureOn(for featureFlag: Flag, allowOverride: Bool) -> Bool { true } + + func getCohortIfEnabled(_ subfeature: any PrivacySubfeature) -> CohortID? { + return nil + } + + func getCohortIfEnabled(for featureFlag: Flag) -> (any FlagCohort)? where Flag: FeatureFlagExperimentDescribing { + return nil + } + + func getAllActiveExperiments() -> Experiments { + return [:] + } } diff --git a/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift b/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift index d50a621f91..1f3128ec92 100644 --- a/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift +++ b/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift @@ -67,6 +67,18 @@ class PrivacyConfigurationMock: PrivacyConfiguration { return .disabled(.disabledInConfig) // this is not used in platform tests, so mocking this poorly for now } + func stateFor(subfeatureID: SubfeatureID, parentFeatureID: ParentFeatureID, versionProvider: AppVersionProvider, randomizer: (Range) -> Double) -> PrivacyConfigurationFeatureState { + return .disabled(.disabledInConfig) + } + + func cohorts(for subfeature: any PrivacySubfeature) -> [PrivacyConfigurationData.Cohort]? { + return nil + } + + func cohorts(subfeatureID: SubfeatureID, parentFeatureID: ParentFeatureID) -> [PrivacyConfigurationData.Cohort]? { + return nil + } + var protectedDomains = Set() func isProtected(domain: String?) -> Bool { return protectedDomains.contains(domain ?? "") From 389b441a2488a7744dd55a19ba1641a0967ad2e8 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 29 Nov 2024 15:54:24 +0100 Subject: [PATCH 10/12] Update BSK with version including Privacy Stats module for use in the macOS browser (#3651) Task/Issue URL: https://app.asana.com/0/72649045549333/1208246350498754/f Description: This change is transparent to iOS since the new code isn't used. The bump to C-S-S contains fixes to macOS's HTML New Tab Page. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a5f642bd6a..cd6978afcc 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -11251,7 +11251,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 214.0.0; + version = 215.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 484c9ae4c6..f150a7d158 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" : "09fd124d27f0588a4296affcd07ce95e40f71a61", - "version" : "214.0.0" + "revision" : "dfd266ab6550902dc2511714f85e2cc03009c057", + "version" : "215.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "dfef00ef77f5181d1d8a4f7cc88f7b7c0514dd34", - "version" : "6.39.0" + "revision" : "c4bb146afdf0c7a93fb9a7d95b1cb255708a470d", + "version" : "6.41.0" } }, { From 86d978aff33dac41fa5d700ce2cc9d2feda168db Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Fri, 29 Nov 2024 18:58:55 +0100 Subject: [PATCH 11/12] Remove incorrect tests and reenable some of the non-flaky ones (#3653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/856498667320406/1208838718874817/f - https://app.asana.com/0/856498667320406/1208796111998084/f Tech Design URL: CC: **Description**: Remove incorrect tests and reenable some of the non-flaky ones **Steps to test this PR**: Check the Ci. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- DuckDuckGo.xcodeproj/project.pbxproj | 4 - DuckDuckGoTests/MockTabDelegate.swift | 8 + DuckDuckGoTests/PixelTests.swift | 4 +- DuckDuckGoTests/SpecialErrorPageTests.swift | 304 -------------------- 4 files changed, 10 insertions(+), 310 deletions(-) delete mode 100644 DuckDuckGoTests/SpecialErrorPageTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index cd6978afcc..826c5ccf05 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -982,7 +982,6 @@ CBAA195A27BFE15600A4BD49 /* NSManagedObjectContextExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */; }; CBC83E3429B631780008E19C /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = CBC83E3329B631780008E19C /* Configuration */; }; CBC88EE12C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */; }; - CBC88EE32C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE22C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift */; }; CBC88EE52C8097B500F0F8C5 /* URLCredentialCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */; }; CBCCF96828885DEE006F4A71 /* AppPrivacyConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C4BC3127C3F9B600C40026 /* AppPrivacyConfigurationTests.swift */; }; CBD4F13C279EBF4A00B20FD7 /* HomeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */; }; @@ -2817,7 +2816,6 @@ CBB6B2542AF6D543006B777C /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = ""; }; CBC7AB542AF6D583008CB798 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScriptTests.swift; sourceTree = ""; }; - CBC88EE22C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageTests.swift; sourceTree = ""; }; CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLCredentialCreator.swift; sourceTree = ""; }; CBC8DC252AF6D4CD00BA681A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessage.swift; sourceTree = ""; }; @@ -6001,7 +5999,6 @@ F189AED61F18F6DE001EBAE1 /* TabTests.swift */, D625AAEA2BBEEFC900BC189A /* TabURLInterceptorTests.swift */, CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */, - CBC88EE22C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift */, ); name = Tabs; sourceTree = ""; @@ -8157,7 +8154,6 @@ CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */, C158AC7B297AB5DC0008723A /* MockSecureVault.swift in Sources */, 569437342BE4E41500C0881B /* SyncErrorHandlerSyncErrorsAlertsTests.swift in Sources */, - CBC88EE32C7F8B1700F0F8C5 /* SpecialErrorPageTests.swift in Sources */, 85C11E4120904BBE00BFFEB4 /* VariantManagerTests.swift in Sources */, F1134ECE1F40EA9C00B73467 /* AtbParserTests.swift in Sources */, F189AEE41F18FDAF001EBAE1 /* LinkTests.swift in Sources */, diff --git a/DuckDuckGoTests/MockTabDelegate.swift b/DuckDuckGoTests/MockTabDelegate.swift index fefb80faa7..acb7db0e53 100644 --- a/DuckDuckGoTests/MockTabDelegate.swift +++ b/DuckDuckGoTests/MockTabDelegate.swift @@ -115,6 +115,14 @@ final class MockTabDelegate: TabDelegate { } +final class MockCredentialCreator: URLCredentialCreating { + + func urlCredentialFrom(trust: SecTrust?) -> URLCredential? { + return URLCredential(user: "", password: "", persistence: .forSession) + } + +} + extension TabViewController { static func fake( diff --git a/DuckDuckGoTests/PixelTests.swift b/DuckDuckGoTests/PixelTests.swift index 132517dac6..2d4ee61a94 100644 --- a/DuckDuckGoTests/PixelTests.swift +++ b/DuckDuckGoTests/PixelTests.swift @@ -30,7 +30,6 @@ class PixelTests: XCTestCase { let userAgentName = "User-Agent" override func setUpWithError() throws { - throw XCTSkip("Potentially flaky") try super.setUpWithError() Pixel.isDryRun = false @@ -162,7 +161,8 @@ class PixelTests: XCTestCase { wait(for: [expectation], timeout: 5.0) } - func testPixelDebouncePreventsFiringWithinInterval() { + func testPixelDebouncePreventsFiringWithinInterval() throws { + throw XCTSkip("Flaky") let firstFireExpectation = XCTestExpectation(description: "First pixel fire should succeed") let thirdFireExpectation = XCTestExpectation(description: "Third pixel fire should succeed after debounce interval") diff --git a/DuckDuckGoTests/SpecialErrorPageTests.swift b/DuckDuckGoTests/SpecialErrorPageTests.swift deleted file mode 100644 index f3ff737d3b..0000000000 --- a/DuckDuckGoTests/SpecialErrorPageTests.swift +++ /dev/null @@ -1,304 +0,0 @@ -// -// SpecialErrorPageTests.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import XCTest -import WebKit - -@testable import SpecialErrorPages -@testable import DuckDuckGo - -class MockSpecialErrorWebView: WKWebView { - - var loadRequestHandler: ((URLRequest, String) -> Void)? - var currentURL: URL? - - override func loadSimulatedRequest(_ request: URLRequest, responseHTML string: String) -> WKNavigation { - loadRequestHandler?(request, string) - return super.loadSimulatedRequest(request, responseHTML: string) - } - - override var url: URL? { - return currentURL - } - - func setCurrentURL(_ url: URL) { - self.currentURL = url - } - -} - -final class SpecialErrorPageTests: XCTestCase { - - var webView: MockSpecialErrorWebView! - var sut: TabViewController! - - override func setUpWithError() throws { - throw XCTSkip("Potentially Flaky") - - try super.setUpWithError() - let featureFlagger = MockFeatureFlagger() - featureFlagger.enabledFeatureFlags = [.sslCertificatesBypass] - sut = .fake(customWebView: { [weak self] configuration in - guard let self else { fatalError("It has to exist") } - self.webView = MockSpecialErrorWebView(frame: CGRect(), configuration: configuration) - return self.webView - }, featureFlagger: featureFlagger) - WKNavigation.swizzleDealloc() - } - - override func tearDown() async throws { - try await super.tearDown() - WKNavigation.restoreDealloc() - } - - func testWhenCertificateExpiredThenExpectedErrorPageIsShown() { - // GIVEN - let error = NSError(domain: "test", - code: NSURLErrorServerCertificateUntrusted, - userInfo: ["_kCFStreamErrorCodeKey": errSSLCertExpired, - NSURLErrorFailingURLErrorKey: URL(string: "https://expired.badssl.com")!]) - let expectation = self.expectation(description: "Special error page should be loaded") - var didFulfill = false - webView.loadRequestHandler = { request, html in - if !didFulfill { - XCTAssertTrue(html.contains("Warning: This site may be insecure")) - XCTAssertTrue(html.contains("is expired")) - XCTAssertEqual(request.url!.host, URL(string: "https://expired.badssl.com")!.host) - expectation.fulfill() - didFulfill = true - } - } - - // WHEN - sut.webView(webView, didFailProvisionalNavigation: WKNavigation(), withError: error) - - // THEN - XCTAssertEqual(sut.failedURL, URL(string: "https://expired.badssl.com")!) - XCTAssertEqual(sut.errorData, SpecialErrorData(kind: .ssl, - errorType: "expired", - domain: "expired.badssl.com", - eTldPlus1: "badssl.com")) - waitForExpectations(timeout: 1) { error in - XCTAssertNil(error, "Expectation was not fulfilled in time") - } - } - - func testWhenCertificateWrongHostThenExpectedErrorPageIsShown() { - // GIVEN - let error = NSError(domain: "test", - code: NSURLErrorServerCertificateUntrusted, - userInfo: ["_kCFStreamErrorCodeKey": errSSLHostNameMismatch, - NSURLErrorFailingURLErrorKey: URL(string: "https://wrong.host.badssl.com")!]) - let expectation = self.expectation(description: "Special error page should be loaded") - var didFulfill = false - webView.loadRequestHandler = { request, html in - if !didFulfill { - XCTAssertTrue(html.contains("Warning: This site may be insecure")) - XCTAssertTrue(html.contains("does not match")) - XCTAssertEqual(request.url!.host, URL(string: "https://wrong.host.badssl.com")!.host) - expectation.fulfill() - didFulfill = true - } - } - - // WHEN - sut.webView(webView, didFailProvisionalNavigation: WKNavigation(), withError: error) - - // THEN - XCTAssertEqual(sut.failedURL, URL(string: "https://wrong.host.badssl.com")!) - XCTAssertEqual(sut.errorData, SpecialErrorData(kind: .ssl, - errorType: "wrongHost", - domain: "wrong.host.badssl.com", - eTldPlus1: "badssl.com")) - waitForExpectations(timeout: 1) { error in - XCTAssertNil(error, "Expectation was not fulfilled in time") - } - } - - func testWhenCertificateSelfSignedThenExpectedErrorPageIsShown() { - // GIVEN - let error = NSError(domain: "test", - code: NSURLErrorServerCertificateUntrusted, - userInfo: ["_kCFStreamErrorCodeKey": errSSLXCertChainInvalid, - NSURLErrorFailingURLErrorKey: URL(string: "https://self-signed.badssl.com")!]) - let expectation = self.expectation(description: "Special error page should be loaded") - var didFulfill = false - webView.loadRequestHandler = { request, html in - if !didFulfill { - XCTAssertTrue(html.contains("Warning: This site may be insecure")) - XCTAssertTrue(html.contains("is not trusted")) - XCTAssertEqual(request.url!.host, URL(string: "https://self-signed.badssl.com")!.host) - expectation.fulfill() - didFulfill = true - } - } - - // WHEN - sut.webView(webView, didFailProvisionalNavigation: WKNavigation(), withError: error) - - // THEN - XCTAssertEqual(sut.failedURL, URL(string: "https://self-signed.badssl.com")!) - XCTAssertEqual(sut.errorData, SpecialErrorData(kind: .ssl, - errorType: "selfSigned", - domain: "self-signed.badssl.com", - eTldPlus1: "badssl.com")) - waitForExpectations(timeout: 1) { error in - XCTAssertNil(error, "Expectation was not fulfilled in time") - } - } - - func testWhenOtherCertificateIssueThenExpectedErrorPageIsShown() { - // GIVEN - let error = NSError(domain: "test", - code: NSURLErrorServerCertificateUntrusted, - userInfo: ["_kCFStreamErrorCodeKey": errSSLUnknownRootCert, - NSURLErrorFailingURLErrorKey: URL(string: "https://untrusted-root.badssl.com")!]) - let expectation = self.expectation(description: "Special error page should be loaded") - var didFulfill = false - webView.loadRequestHandler = { request, html in - if !didFulfill { - XCTAssertTrue(html.contains("Warning: This site may be insecure")) - XCTAssertTrue(html.contains("is not trusted")) - XCTAssertEqual(request.url!.host, URL(string: "https://untrusted-root.badssl.com")!.host) - expectation.fulfill() - didFulfill = true - } - } - - // WHEN - sut.webView(webView, didFailProvisionalNavigation: WKNavigation(), withError: error) - - // THEN - XCTAssertEqual(sut.failedURL, URL(string: "https://untrusted-root.badssl.com")!) - XCTAssertEqual(sut.errorData, SpecialErrorData(kind: .ssl, - errorType: "invalid", - domain: "untrusted-root.badssl.com", - eTldPlus1: "badssl.com")) - waitForExpectations(timeout: 1) { error in - XCTAssertNil(error, "Expectation was not fulfilled in time") - } - } - - @MainActor - func testWhenNavigationEndedIfNoSSLFailureSSLUserScriptIsNotEnabled() { - // GIVEN - webView.setCurrentURL(URL(string: "https://self-signed.badssl.com")!) - sut.storedSpecialErrorPageUserScript = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") - - // WHEN - sut.webView(webView, didFinish: WKNavigation()) - - // THEN - XCTAssertFalse(sut.specialErrorPageUserScript?.isEnabled ?? true) - } - - @MainActor - func testWhenNavigationEndedIfSSLFailureButURLIsDifferentFromNavigationURLThenSSLUserScriptIsNotEnabled() { - // GIVEN - webView.setCurrentURL(URL(string: "https://self-signed.badssl.com")!) - sut.failedURL = URL(string: "https://different.url.com")! - sut.storedSpecialErrorPageUserScript = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") - - // WHEN - sut.webView(webView, didFinish: WKNavigation()) - - // THEN - XCTAssertFalse(sut.specialErrorPageUserScript?.isEnabled ?? true) - } - - @MainActor - func testWhenNavigationEndedIfSSLFailureAndNavigationURLIsTheSameAsFailingURLThenSSLUserScriptIsEnabled() { - // GIVEN - webView.setCurrentURL(URL(string: "https://self-signed.badssl.com")!) - sut.failedURL = URL(string: "https://self-signed.badssl.com")! - sut.storedSpecialErrorPageUserScript = SpecialErrorPageUserScript(localeStrings: "", languageCode: "") - - // WHEN - sut.webView(webView, didFinish: WKNavigation()) - - // THEN - XCTAssertTrue(sut.specialErrorPageUserScript?.isEnabled ?? false) - } - - func testWhenDidReceiveChallengeIfChallengeForCertificateValidationAndNoBypassThenShouldNotReturnCredentials() async { - let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) - let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallengeSender()) - await sut.webView(webView, didReceive: challenge) { _, credential in - XCTAssertNil(credential) - } - } - - func testWhenDidReceiveChallengeIfChallengeForCertificateValidationAndUserRequestBypassThenReturnsCredentials() async { - let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) - let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallengeSender()) - await sut.visitSite() - await sut.webView(webView, didReceive: challenge) { _, credential in - XCTAssertNotNil(credential) - } - } - -} - -final class ChallengeSender: URLAuthenticationChallengeSender { - func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {} - func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {} - func cancel(_ challenge: URLAuthenticationChallenge) {} - func isEqual(_ object: Any?) -> Bool { - return false - } - var hash: Int = 0 - var superclass: AnyClass? - func `self`() -> Self { - self - } - func perform(_ aSelector: Selector!) -> Unmanaged! { - return nil - } - func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged! { - return nil - } - func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged! { - return nil - } - func isProxy() -> Bool { - return false - } - func isKind(of aClass: AnyClass) -> Bool { - return false - } - func isMember(of aClass: AnyClass) -> Bool { - return false - } - func conforms(to aProtocol: Protocol) -> Bool { - return false - } - func responds(to aSelector: Selector!) -> Bool { - return false - } - var description: String = "" -} - -final class MockCredentialCreator: URLCredentialCreating { - - func urlCredentialFrom(trust: SecTrust?) -> URLCredential? { - return URLCredential(user: "", password: "", persistence: .forSession) - } - -} From 7e360588ecc30ba1c1ae21c2a2e56283b86cc3e1 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 29 Nov 2024 15:52:27 -0800 Subject: [PATCH 12/12] Add support for RMF survey `locale` parameter (#3648) Task/Issue URL: https://app.asana.com/0/1199333091098016/1208868981778301/f Tech Design URL: CC: Description: This PR updates BSK to pull in the survey builder's new locale support. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 826c5ccf05..6ba7046c3e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -11247,7 +11247,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 215.0.0; + version = 215.0.1; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f150a7d158..d6698fc14d 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" : "dfd266ab6550902dc2511714f85e2cc03009c057", - "version" : "215.0.0" + "revision" : "837dfbfe7a1b2a5e0ec2fb24a47a53dec53444b0", + "version" : "215.0.1" } }, {