diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 6cdb4ec67a..f79c653987 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.136.0 +MARKETING_VERSION = 7.137.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index a049977e45..bf0b7a567d 100644 --- a/Core/AppPrivacyConfigurationDataProvider.swift +++ b/Core/AppPrivacyConfigurationDataProvider.swift @@ -23,8 +23,8 @@ import BrowserServicesKit final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"38047fbabac1af7f77112ee692d5d481\"" - public static let embeddedDataSHA = "ee998861bbed8b784a7f19caafd76a8f6eb9a82160b0b6ddb21d97e67332b38f" + public static let embeddedDataETag = "\"9087766799743533c0741b03cea431d1\"" + public static let embeddedDataSHA = "9e9fcfd329fc587ba732cf9cb7e71d81f7af7717c3f804f28b9c8603599ee8d8" } public var embeddedDataEtag: String { diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 0fe7590da2..763b282091 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -41,6 +41,7 @@ public enum FeatureFlag: String { case syncPromotionBookmarks case syncPromotionPasswords case onboardingHighlights + case autofillSurveys } extension FeatureFlag: FeatureFlagSourceProviding { @@ -77,15 +78,17 @@ extension FeatureFlag: FeatureFlagSourceProviding { case .newTabPageSections: return .remoteDevelopment(.feature(.newTabPageImprovements)) case .duckPlayer: - return .remoteReleasable(.feature(.duckPlayer)) + return .remoteReleasable(.subfeature(DuckPlayerSubfeature.enableDuckPlayer)) case .sslCertificatesBypass: - return .remoteReleasable(.subfeature(sslCertificatesSubfeature.allowBypass)) + return .remoteReleasable(.subfeature(SslCertificatesSubfeature.allowBypass)) case .syncPromotionBookmarks: return .remoteReleasable(.subfeature(SyncPromotionSubfeature.bookmarks)) case .syncPromotionPasswords: return .remoteReleasable(.subfeature(SyncPromotionSubfeature.passwords)) case .onboardingHighlights: return .internalOnly + case .autofillSurveys: + return .remoteReleasable(.feature(.autofillSurveys)) } } } diff --git a/Core/HistoryManager.swift b/Core/HistoryManager.swift index 272bee87dd..0020468071 100644 --- a/Core/HistoryManager.swift +++ b/Core/HistoryManager.swift @@ -88,7 +88,7 @@ public class HistoryManager: HistoryManaging { let baseDomain = tld.eTLDplus1(domain) else { return } await withCheckedContinuation { continuation in - historyCoordinator.burnDomains([baseDomain], tld: tld) { + historyCoordinator.burnDomains([baseDomain], tld: tld) { _ in continuation.resume() } } @@ -137,8 +137,8 @@ class NullHistoryCoordinator: HistoryCoordinating { completion() } - func burnDomains(_ baseDomains: Set, tld: Common.TLD, completion: @escaping () -> Void) { - completion() + func burnDomains(_ baseDomains: Set, tld: Common.TLD, completion: @escaping (Set) -> Void) { + completion([]) } func burnVisits(_ visits: [History.Visit], completion: @escaping () -> Void) { diff --git a/Core/MarketplaceAdPostback.swift b/Core/MarketplaceAdPostback.swift new file mode 100644 index 0000000000..d8d9a5ff5a --- /dev/null +++ b/Core/MarketplaceAdPostback.swift @@ -0,0 +1,93 @@ +// +// MarketplaceAdPostback.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 StoreKit +import AdAttributionKit + +enum MarketplaceAdPostback { + case installNewUser + case installReturningUser + + /// An enumeration representing coarse conversion values for both SKAdNetwork and AdAttributionKit. + /// + /// This enum provides a unified interface to handle coarse conversion values, which are used in both SKAdNetwork and AdAttributionKit. + /// Despite having the same value names (`low`, `medium`, `high`), the types for these values differ between the two frameworks. + /// This wrapper simplifies the usage by providing a common interface. + /// + /// - Cases: + /// - `low`: Represents a low conversion value. + /// - `medium`: Represents a medium conversion value. + /// - `high`: Represents a high conversion value. + /// + /// - Properties: + /// - `coarseConversionValue`: Available on iOS 17.4 and later, this property returns the corresponding `CoarseConversionValue` from AdAttributionKit. + /// - `skAdCoarseConversionValue`: Available on iOS 16.1 and later, this property returns the corresponding `SKAdNetwork.CoarseConversionValue`. + /// + enum CoarseConversion { + case low + case medium + case high + + /// Returns the corresponding `CoarseConversionValue` from AdAttributionKit. + @available(iOS 17.4, *) + var coarseConversionValue: CoarseConversionValue { + switch self { + case .low: return .low + case .medium: return .medium + case .high: return .high + } + } + + /// Returns the corresponding `SKAdNetwork.CoarseConversionValue`. + @available(iOS 16.1, *) + var skAdCoarseConversionValue: SKAdNetwork.CoarseConversionValue { + switch self { + case .low: return .low + case .medium: return .medium + case .high: return .high + } + } + } + + // https://app.asana.com/0/0/1208126219488943/f + var fineValue: Int { + switch self { + case .installNewUser: return 0 + case .installReturningUser: return 1 + } + } + + var coarseValue: CoarseConversion { + switch self { + case .installNewUser: return .high + case .installReturningUser: return .low + } + } + + @available(iOS 17.4, *) + var adAttributionKitCoarseValue: CoarseConversionValue { + return coarseValue.coarseConversionValue + } + + @available(iOS 16.1, *) + var SKAdCoarseValue: SKAdNetwork.CoarseConversionValue { + return coarseValue.skAdCoarseConversionValue + } +} diff --git a/Core/MarketplaceAdPostbackManager.swift b/Core/MarketplaceAdPostbackManager.swift new file mode 100644 index 0000000000..32cfbdea7f --- /dev/null +++ b/Core/MarketplaceAdPostbackManager.swift @@ -0,0 +1,74 @@ +// +// MarketplaceAdPostbackManager.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 + +public protocol MarketplaceAdPostbackManaging { + + /// Updates the install postback based on the return user measurement + /// + /// This method determines whether the user is a returning user or a new user and sends the appropriate postback value: + /// - If the user is returning, it sends the `appLaunchReturningUser` postback value. + /// - If the user is new, it sends the `appLaunchNewUser` postback value. + /// + /// > For the time being, we're also sending `lockPostback` to `true`. + /// > More information can be found [here](https://app.asana.com/0/0/1208126219488943/1208289369964239/f). + func sendAppLaunchPostback() + + /// Updates the stored value for the returning user state. + /// + /// This method updates the storage with the current state of the user (returning or new). + /// Since `ReturnUserMeasurement` will always return `isReturningUser` as `false` after the first run, + /// `MarketplaceAdPostbackManaging` maintains its own storage of the user's state across app launches. + func updateReturningUserValue() +} + +public struct MarketplaceAdPostbackManager: MarketplaceAdPostbackManaging { + private let storage: MarketplaceAdPostbackStorage + private let updater: MarketplaceAdPostbackUpdating + private let returningUserMeasurement: ReturnUserMeasurement + + internal init(storage: MarketplaceAdPostbackStorage = UserDefaultsMarketplaceAdPostbackStorage(), + updater: MarketplaceAdPostbackUpdating = MarketplaceAdPostbackUpdater(), + returningUserMeasurement: ReturnUserMeasurement = KeychainReturnUserMeasurement()) { + self.storage = storage + self.updater = updater + self.returningUserMeasurement = returningUserMeasurement + } + + public init() { + self.storage = UserDefaultsMarketplaceAdPostbackStorage() + self.updater = MarketplaceAdPostbackUpdater() + self.returningUserMeasurement = KeychainReturnUserMeasurement() + } + + public func sendAppLaunchPostback() { + guard let isReturningUser = storage.isReturningUser else { return } + + if isReturningUser { + updater.updatePostback(.installReturningUser, lockPostback: true) + } else { + updater.updatePostback(.installNewUser, lockPostback: true) + } + } + + public func updateReturningUserValue() { + storage.updateReturningUserValue(returningUserMeasurement.isReturningUser) + } +} diff --git a/Core/MarketplaceAdPostbackStorage.swift b/Core/MarketplaceAdPostbackStorage.swift new file mode 100644 index 0000000000..66734f1818 --- /dev/null +++ b/Core/MarketplaceAdPostbackStorage.swift @@ -0,0 +1,62 @@ +// +// MarketplaceAdPostbackStorage.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 + +/// A protocol defining the storage for marketplace ad postback data. +protocol MarketplaceAdPostbackStorage { + + /// A Boolean value indicating whether the user is a returning user. + /// + /// If the value is `nil`, it means the storage was never set. + var isReturningUser: Bool? { get } + + /// Updates the stored value indicating whether the user is a returning user. + /// + /// - Parameter value: A Boolean value indicating whether the user is a returning user. + func updateReturningUserValue(_ value: Bool) +} + +/// A concrete implementation of `MarketplaceAdPostbackStorage` that uses `UserDefaults` for storage. +struct UserDefaultsMarketplaceAdPostbackStorage: MarketplaceAdPostbackStorage { + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + var isReturningUser: Bool? { + userDefaults.isReturningUser + } + + func updateReturningUserValue(_ value: Bool) { + userDefaults.isReturningUser = value + } +} + +private extension UserDefaults { + enum Keys { + static let isReturningUser = "marketplaceAdPostback.isReturningUser" + } + + var isReturningUser: Bool? { + get { object(forKey: Keys.isReturningUser) as? Bool } + set { set(newValue, forKey: Keys.isReturningUser) } + } +} diff --git a/Core/MarketplaceAdPostbackUpdater.swift b/Core/MarketplaceAdPostbackUpdater.swift new file mode 100644 index 0000000000..0b8d3a819b --- /dev/null +++ b/Core/MarketplaceAdPostbackUpdater.swift @@ -0,0 +1,81 @@ +// +// MarketplaceAdPostbackUpdater.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 AdAttributionKit +import os.log +import StoreKit + +/// Updates anonymous attribution values. +/// +/// DuckDuckGo uses the SKAdNetwork framework to monitor anonymous install attribution data. +/// No personally identifiable data is involved. +/// DuckDuckGo does not use the App Tracking Transparency framework at any point. +/// See https://developer.apple.com/documentation/storekit/skadnetwork/ for details. +/// + +protocol MarketplaceAdPostbackUpdating { + func updatePostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) +} + +struct MarketplaceAdPostbackUpdater: MarketplaceAdPostbackUpdating { + func updatePostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) { +#if targetEnvironment(simulator) + Logger.general.debug("Attribution: Postback doesn't work on simulators, returning early...") +#else + if #available(iOS 17.4, *) { + // https://developer.apple.com/documentation/adattributionkit/adattributionkit-skadnetwork-interoperability + Task { + await updateAdAttributionKitPostback(postback, lockPostback: lockPostback) + } + updateSKANPostback(postback, lockPostback: lockPostback) + } else if #available(iOS 16.1, *) { + updateSKANPostback(postback, lockPostback: lockPostback) + } +#endif + } + + @available(iOS 17.4, *) + private func updateAdAttributionKitPostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) async { + do { + try await AdAttributionKit.Postback.updateConversionValue(postback.fineValue, + coarseConversionValue: postback.adAttributionKitCoarseValue, + lockPostback: lockPostback) + Logger.general.debug("Attribution: AdAttributionKit postback succeeded") + } catch { + Logger.general.error("Attribution: AdAttributionKit postback failed \(String(describing: error), privacy: .public)") + } + } + + @available(iOS 16.1, *) + private func updateSKANPostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) { + /// Switched to using the completion handler API instead of async due to an encountered error. + /// Error report: + /// https://errors.duckduckgo.com/organizations/ddg/issues/104096/events/ab29c80e711f11efbf32499bdc26619c/ + + SKAdNetwork.updatePostbackConversionValue(postback.fineValue, + coarseValue: postback.SKAdCoarseValue) { error in + if let error = error { + Logger.general.error("Attribution: SKAN 4 postback failed \(String(describing: error), privacy: .public)") + } else { + Logger.general.debug("Attribution: SKAN 4 postback succeeded") + } + } + } +} diff --git a/Core/NSAttributedStringExtension.swift b/Core/NSAttributedStringExtension.swift index f0a3a61be7..c4beb2b327 100644 --- a/Core/NSAttributedStringExtension.swift +++ b/Core/NSAttributedStringExtension.swift @@ -40,4 +40,86 @@ extension NSAttributedString { newString.setAttributes([.font: newFont], range: string.fullRange) return newString } + + /// Creates a new `NSAttributedString` initialized with the characters and attributes of the current attributed string plus the specified font. + /// + /// - Parameter font: The `UIFont` to apply to the text in the `NSAttributedString`. + /// - Returns: A new `NSAttributedString`initialized with characters and attributes of the current attributed string plus the specified font. + public func withFont(_ font: UIFont) -> NSAttributedString { + with(attribute: .font, value: font) + } + + /// Creates a new `NSAttributedString` initialized with the characters and attributes of the current attributed string plus the specified text color + /// + /// - Parameter color: The color to apply to the text + /// - Returns: A new `NSAttributedString` initialized with characters and attributes of the current attributed string plus the text color + public func withTextColor(_ color: UIColor) -> NSAttributedString { + with(attribute: .foregroundColor, value: color) + } + + /// Creates a new `NSAttributedString` initialized with the characters and attributes of the current attributed string plus the specified attribute + /// + /// - Parameters: + /// - key: The attribute key to apply. This should be one of the keys defined in `NSAttributedString.Key`. + /// - value: The value associated with the attribute key. This can be any object compatible with the attribute. + /// - range: An optional `NSRange` specifying the range within the `NSAttributedString` to apply the attribute. + /// If `nil`, the attribute is applied to the entire `NSAttributedString`. + /// - Returns: A new `NSAttributedString` with the specified attribute applied. + public func with(attribute key: NSAttributedString.Key, value: Any, in range: NSRange? = nil) -> NSAttributedString { + with(attributes: [key: value], in: range) + } + + /// Creates a new `NSAttributedString` initialized with the characters and attributes of the current attributed string plus the specified attributes + /// + /// - Parameters: + /// - attributes: A dictionary of attributes to apply, where the keys are of type `NSAttributedString.Key` and the values + /// are objects compatible with the attributes (e.g., `UIFont`, `UIColor`). + /// - range: An optional `NSRange` specifying the range within the `NSAttributedString` to apply the attributes. + /// If `nil`, the attributes are applied to the entire `NSAttributedString`. + /// - Returns: A new `NSAttributedString` with the specified attributes applied. + public func with(attributes: [NSAttributedString.Key: Any], in range: NSRange? = nil) -> NSAttributedString { + let mutableString = NSMutableAttributedString(attributedString: self) + mutableString.addAttributes(attributes, range: range ?? string.nsRange) + return mutableString + } +} + +/// Concatenates two `NSAttributedString` instances, returning a new `NSAttributedString`. +/// +/// - Parameters: +/// - lhs: The left-hand side `NSAttributedString` to which the `rhs` `NSAttributedString` will be appended. +/// - rhs: The `NSAttributedString` to append to the `lhs` `NSAttributedString`. +/// - Returns: A new `NSAttributedString` that is the result of concatenating `lhs` and `rhs`. +public func + (lhs: NSAttributedString, rhs: NSAttributedString) -> NSAttributedString { + let mutable = NSMutableAttributedString(attributedString: lhs) + mutable.append(rhs) + return mutable +} + +/// Concatenates an `NSAttributedString` with a `String`, returning a new `NSAttributedString`. +/// +/// - Parameters: +/// - lhs: The left-hand side `NSAttributedString` to which the `String` will be appended. +/// - rhs: The `String` to append to the `lhs` `NSAttributedString`. +/// - Returns: A new `NSAttributedString` which is the result of concatenating `lhs` with `rhs`. +public func + (lhs: NSAttributedString, rhs: String) -> NSAttributedString { + lhs + NSAttributedString(string: rhs) +} + +/// Concatenates a `String` with an `NSAttributedString`, returning a new `NSAttributedString`. +/// +/// - Parameters: +/// - lhs: The `String` to prepend to the `rhs` `NSAttributedString`. +/// - rhs: The right-hand side `NSAttributedString` that will be appended to the `lhs` `String`. +/// - Returns: A new `NSAttributedString` which is the result of concatenating `lhs` with `rhs`. +public func + (lhs: String, rhs: NSAttributedString) -> NSAttributedString { + NSAttributedString(string: lhs) + rhs +} + +private extension String { + + var nsRange: NSRange { + NSRange(startIndex..., in: self) + } + } diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index b5b6b5384c..2afe5960b8 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -298,6 +298,8 @@ extension Pixel { case autofillLoginsReportConfirmationPromptConfirmed case autofillLoginsReportConfirmationPromptDismissed + case autofillManagementScreenVisitSurveyAvailable + case getDesktopCopy case getDesktopShare @@ -723,7 +725,6 @@ extension Pixel { // MARK: Apple Ad Attribution case appleAdAttribution - case appleAdAttributionNotAttributed // MARK: Secure Vault case secureVaultL1KeyMigration @@ -1105,9 +1106,11 @@ extension Pixel.Event { case .autofillLoginsReportFailure: return "autofill_logins_report_failure" case .autofillLoginsReportAvailable: return "autofill_logins_report_available" - case .autofillLoginsReportConfirmationPromptDisplayed: return "autofill_logins_report_confirmation_prompt_displayed" - case .autofillLoginsReportConfirmationPromptConfirmed: return "autofill_logins_report_confirmation_prompt_confirmed" - case .autofillLoginsReportConfirmationPromptDismissed: return "autofill_logins_report_confirmation_prompt_dismissed" + case .autofillLoginsReportConfirmationPromptDisplayed: return "autofill_logins_report_confirmation_displayed" + case .autofillLoginsReportConfirmationPromptConfirmed: return "autofill_logins_report_confirmation_confirmed" + case .autofillLoginsReportConfirmationPromptDismissed: return "autofill_logins_report_confirmation_dismissed" + + case .autofillManagementScreenVisitSurveyAvailable: return "m_autofill_management_screen_visit_survey_available" case .getDesktopCopy: return "m_get_desktop_copy" case .getDesktopShare: return "m_get_desktop_share" @@ -1442,7 +1445,6 @@ extension Pixel.Event { // MARK: - Apple Ad Attribution case .appleAdAttribution: return "m_apple-ad-attribution" - case .appleAdAttributionNotAttributed: return "m_apple-ad-attribution_not-attributed" // MARK: - User behavior case .userBehaviorReloadTwiceWithin12Seconds: return "m_reload-twice-within-12-seconds" diff --git a/Core/SyncErrorHandler.swift b/Core/SyncErrorHandler.swift index 3c29fb3a99..a3ff07e794 100644 --- a/Core/SyncErrorHandler.swift +++ b/Core/SyncErrorHandler.swift @@ -39,21 +39,21 @@ public enum AsyncErrorType: String { public class SyncErrorHandler: EventMapping { @UserDefaultsWrapper(key: .syncBookmarksPaused, defaultValue: false) - private (set) public var isSyncBookmarksPaused: Bool { + private(set) public var isSyncBookmarksPaused: Bool { didSet { isSyncPausedChangedPublisher.send() } } @UserDefaultsWrapper(key: .syncCredentialsPaused, defaultValue: false) - private (set) public var isSyncCredentialsPaused: Bool { + private(set) public var isSyncCredentialsPaused: Bool { didSet { isSyncPausedChangedPublisher.send() } } @UserDefaultsWrapper(key: .syncIsPaused, defaultValue: false) - private (set) public var isSyncPaused: Bool { + private(set) public var isSyncPaused: Bool { didSet { isSyncPausedChangedPublisher.send() } diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 1c1b5216a7..11fcd46d61 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -92,6 +92,7 @@ public struct UserDefaultsWrapper { case autofillSearchDauDate = "com.duckduckgo.app.autofill.SearchDauDate" case autofillFillDate = "com.duckduckgo.app.autofill.FillDate" case autofillOnboardedUser = "com.duckduckgo.app.autofill.OnboardedUser" + case autofillSurveysCompleted = "com.duckduckgo.app.autofill.SurveysCompleted" case syncPromoBookmarksDismissed = "com.duckduckgo.app.sync.PromoBookmarksDismissed" case syncPromoPasswordsDismissed = "com.duckduckgo.app.sync.PromoPasswordsDismissed" @@ -133,8 +134,6 @@ public struct UserDefaultsWrapper { case addressBarPosition = "com.duckduckgo.ios.addressbarposition" case showFullURLAddress = "com.duckduckgo.ios.showfullurladdress" - case webContainerId = "com.duckduckgo.ios.webcontainer.id" - case bookmarksLastGoodVersion = "com.duckduckgo.ios.bookmarksLastGoodVersion" case bookmarksMigrationVersion = "com.duckduckgo.ios.bookmarksMigrationVersion" diff --git a/Core/WKWebViewConfigurationExtension.swift b/Core/WKWebViewConfigurationExtension.swift index 32a34285b2..18de2c2dad 100644 --- a/Core/WKWebViewConfigurationExtension.swift +++ b/Core/WKWebViewConfigurationExtension.swift @@ -18,15 +18,16 @@ // import WebKit +import Persistence extension WKWebViewConfiguration { @MainActor - public static func persistent(idManager: DataStoreIdManager = .shared) -> WKWebViewConfiguration { + public static func persistent(idManager: DataStoreIdManaging = DataStoreIdManager.shared) -> WKWebViewConfiguration { let config = configuration(persistsData: true) // Only use a container if there's an id which will be allocated next time the fire button is used. - if #available(iOS 17, *), let containerId = idManager.id { + if #available(iOS 17, *), let containerId = idManager.currentId { config.websiteDataStore = WKWebsiteDataStore(forIdentifier: containerId) } return config @@ -58,32 +59,33 @@ extension WKWebViewConfiguration { public protocol DataStoreIdManaging { - var id: UUID? { get } - var hasId: Bool { get } - func allocateNewContainerId() + var currentId: UUID? { get } + func invalidateCurrentIdAndAllocateNew() } public class DataStoreIdManager: DataStoreIdManaging { - public static let shared = DataStoreIdManager() + enum Constants: String { + case currentWebContainerId = "com.duckduckgo.ios.webcontainer.id" + } - @UserDefaultsWrapper(key: .webContainerId, defaultValue: nil) - private var containerId: String? + public static let shared = DataStoreIdManager() - public var id: UUID? { - if let containerId { - return UUID(uuidString: containerId) - } - return nil + private let store: KeyValueStoring + init(store: KeyValueStoring = UserDefaults.app) { + self.store = store } - public var hasId: Bool { - return containerId != nil + public var currentId: UUID? { + guard let uuidString = store.object(forKey: Constants.currentWebContainerId.rawValue) as? String else { + return nil + } + return UUID(uuidString: uuidString) } - public func allocateNewContainerId() { - self.containerId = UUID().uuidString + public func invalidateCurrentIdAndAllocateNew() { + store.set(UUID().uuidString, forKey: Constants.currentWebContainerId.rawValue) } } diff --git a/Core/WebCacheManager.swift b/Core/WebCacheManager.swift index 356eac3c0d..c43462a36c 100644 --- a/Core/WebCacheManager.swift +++ b/Core/WebCacheManager.swift @@ -24,8 +24,8 @@ import os.log extension WKWebsiteDataStore { - public static func current(dataStoreIdManager: DataStoreIdManager = .shared) -> WKWebsiteDataStore { - if #available(iOS 17, *), let id = dataStoreIdManager.id { + public static func current(dataStoreIdManager: DataStoreIdManaging = DataStoreIdManager.shared) -> WKWebsiteDataStore { + if #available(iOS 17, *), let id = dataStoreIdManager.currentId { return WKWebsiteDataStore(forIdentifier: id) } else { return WKWebsiteDataStore.default() @@ -82,17 +82,13 @@ public class WebCacheManager { dataStoreIdManager: DataStoreIdManaging = DataStoreIdManager.shared) async { var cookiesToUpdate = [HTTPCookie]() - if #available(iOS 17, *), dataStoreIdManager.hasId { + if #available(iOS 17, *) { cookiesToUpdate += await containerBasedClearing(storeIdManager: dataStoreIdManager) ?? [] } // Perform legacy clearing to migrate to new container cookiesToUpdate += await legacyDataClearing() ?? [] - if #available(iOS 17, *) { - dataStoreIdManager.allocateNewContainerId() - } - cookieStorage.updateCookies(cookiesToUpdate, keepingPreservedLogins: logins) } @@ -118,13 +114,24 @@ extension WebCacheManager { @available(iOS 17, *) private func containerBasedClearing(storeIdManager: DataStoreIdManaging) async -> [HTTPCookie]? { - guard let containerId = storeIdManager.id else { return [] } + guard let containerId = storeIdManager.currentId else { + storeIdManager.invalidateCurrentIdAndAllocateNew() + return [] + } + storeIdManager.invalidateCurrentIdAndAllocateNew() + var dataStore: WKWebsiteDataStore? = WKWebsiteDataStore(forIdentifier: containerId) let cookies = await dataStore?.httpCookieStore.allCookies() dataStore = nil - let uuids = await WKWebsiteDataStore.allDataStoreIdentifiers - let previousLeftOversCount = max(0, uuids.count - 1) // -1 because there should be a current store + var uuids = await WKWebsiteDataStore.allDataStoreIdentifiers + if let newContainerID = storeIdManager.currentId, + let newIdIndex = uuids.firstIndex(of: newContainerID) { + assertionFailure("Attempted to cleanup current Data Store") + uuids.remove(at: newIdIndex) + } + + let previousLeftOversCount = max(0, uuids.count - 1) // -1 because one store is expected to be cleared for uuid in uuids { try? await WKWebsiteDataStore.remove(forIdentifier: uuid) } diff --git a/Core/ios-config.json b/Core/ios-config.json index 9bbcb9cdf4..6330f1c820 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1725043256685, + "version": 1725898107484, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -335,6 +335,12 @@ { "domain": "dkb.de" }, + { + "domain": "headphonecheck.com" + }, + { + "domain": "lehmannaudio.com" + }, { "domain": "marvel.com" }, @@ -382,7 +388,7 @@ } } }, - "hash": "5a81046c452177af48157543487a38d5" + "hash": "a235f7ab6a5a3632380570bbc5113d51" }, "autofillBreakageReporter": { "state": "enabled", @@ -449,12 +455,15 @@ }, { "percent": 25 + }, + { + "percent": 100 } ] } } }, - "hash": "471e5069536f5b7824c5e4b60e5be274" + "hash": "28d4af98382248e184c4315bd49f4222" }, "breakageReporting": { "state": "disabled", @@ -6004,6 +6013,16 @@ } ] }, + "adsrvr.org": { + "rules": [ + { + "rule": "js.adsrvr.org/up_loader.1.1.0.js", + "domains": [ + "codot.gov" + ] + } + ] + }, "adswizz.com": { "rules": [ { @@ -8576,16 +8595,6 @@ } ] }, - "siteimproveanalytics.com": { - "rules": [ - { - "rule": "siteimproveanalytics.com", - "domains": [ - "codot.gov" - ] - } - ] - }, "skimresources.com": { "rules": [ { @@ -9173,7 +9182,7 @@ "domain": "capitalone.com" } ], - "hash": "70bdfe11b5235b40019495340bd91154" + "hash": "96d026a445971b650f4a6e899e339ff8" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index bcd2256e9e..e22b0b09e2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -154,10 +154,12 @@ 3158461A281B08F5004ADB8B /* AutofillLoginListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31584619281B08F5004ADB8B /* AutofillLoginListViewModel.swift */; }; 3161D13227AC161B00285CF6 /* DownloadMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3161D13127AC161B00285CF6 /* DownloadMetadata.swift */; }; 31669B9A28020A460071CC18 /* SaveLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31669B9928020A460071CC18 /* SaveLoginViewModel.swift */; }; + 316790E52C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */; }; 316931D727BD10BB0095F5ED /* SaveToDownloadsAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316931D627BD10BB0095F5ED /* SaveToDownloadsAlert.swift */; }; 316931D927BD22A80095F5ED /* DownloadActionMessageViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316931D827BD22A80095F5ED /* DownloadActionMessageViewHelper.swift */; }; 3170048227A9504F00C03F35 /* DownloadMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3170048127A9504F00C03F35 /* DownloadMocks.swift */; }; 317045C02858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */; }; + 317F5F982C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */; }; 31860A5B2C57ED2D005561F5 /* DuckPlayerStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */; }; 31951E8E2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */; }; 319A371028299A850079FBCE /* PasswordHider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319A370F28299A850079FBCE /* PasswordHider.swift */; }; @@ -166,6 +168,9 @@ 31A42564285A09E800049386 /* FaviconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A42563285A09E800049386 /* FaviconView.swift */; }; 31A42566285A0A6300049386 /* FaviconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A42565285A0A6300049386 /* FaviconViewModel.swift */; }; 31B1FA87286EFC5C00CA3C1C /* XCTestCaseExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B1FA86286EFC5C00CA3C1C /* XCTestCaseExtension.swift */; }; + 31B2F10F2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B2F10E2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift */; }; + 31B2F1112C92FEE000CD30E3 /* MarketplaceAdPostback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B2F1102C92FEE000CD30E3 /* MarketplaceAdPostback.swift */; }; + 31B2F1132C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B2F1122C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift */; }; 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B2F11E287846320040427A /* NoMicPermissionAlert.swift */; }; 31B524572715BB23002225AB /* WebJSAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B524562715BB23002225AB /* WebJSAlert.swift */; }; 31BC5F412C2B0B540004DF37 /* DuckPlayer.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 31BC5F402C2B0B540004DF37 /* DuckPlayer.xcassets */; }; @@ -569,6 +574,7 @@ 980891A92238504B00313A70 /* UILabelExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980891A82238504B00313A70 /* UILabelExtension.swift */; }; 9813F79822BA71AA00A80EDB /* StorageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9813F79722BA71AA00A80EDB /* StorageCache.swift */; }; 9817C9C321EF594700884F65 /* AutoClear.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9817C9C221EF594700884F65 /* AutoClear.swift */; }; + 981C49B02C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981C49AF2C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift */; }; 981CA7EA2617797500E119D5 /* MainViewController+AddFavoriteFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981CA7E92617797500E119D5 /* MainViewController+AddFavoriteFlow.swift */; }; 981FED692201FE69008488D7 /* AutoClearSettingsScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981FED682201FE69008488D7 /* AutoClearSettingsScreenTests.swift */; }; 981FED6E22025151008488D7 /* BlankSnapshotViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981FED6C22025151008488D7 /* BlankSnapshotViewController.swift */; }; @@ -696,11 +702,17 @@ 9F69331D2C5A191400CD6A5D /* MockTutorialSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F69331C2C5A191400CD6A5D /* MockTutorialSettings.swift */; }; 9F69331F2C5B1D0C00CD6A5D /* OnFirstAppearViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F69331E2C5B1D0C00CD6A5D /* OnFirstAppearViewModifier.swift */; }; 9F6933212C5B9A5B00CD6A5D /* OnboardingHostingControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6933202C5B9A5B00CD6A5D /* OnboardingHostingControllerMock.swift */; }; + 9F7CFF762C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7CFF752C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift */; }; 9F7CFF782C86E3E10012833E /* OnboardingManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7CFF772C86E3E10012833E /* OnboardingManagerTests.swift */; }; + 9F7CFF7D2C89B69A0012833E /* AppIconPickerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */; }; + 9F7CFF7F2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7CFF7E2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift */; }; 9F8007262C5261AF003EDAF4 /* MockPrivacyDataReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8007252C5261AF003EDAF4 /* MockPrivacyDataReporter.swift */; }; 9F8FE9492BAE50E50071E372 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9F8FE9482BAE50E50071E372 /* Lottie */; }; + 9F96F73B2C9144D5009E45D5 /* Onboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 9F96F73A2C9144D5009E45D5 /* Onboarding */; }; + 9F96F73F2C914C57009E45D5 /* OnboardingGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F96F73E2C914C57009E45D5 /* OnboardingGradient.swift */; }; 9F9A922E2C86A56B001D036D /* OnboardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9A922D2C86A56B001D036D /* OnboardingManager.swift */; }; 9F9A92312C86AAE9001D036D /* OnboardingDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9A92302C86AAE9001D036D /* OnboardingDebugView.swift */; }; + 9F9A92342C86B42B001D036D /* AppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9A92332C86B42B001D036D /* AppIconPicker.swift */; }; 9F9EE4CE2C377D4900D4118E /* OnboardingFirePixelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9EE4CC2C377D3F00D4118E /* OnboardingFirePixelMock.swift */; }; 9F9EE4D42C37BB1300D4118E /* OnboardingView+Landing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */; }; 9FA5E44B2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */; }; @@ -709,9 +721,16 @@ 9FB027192C26BC29009EA190 /* BrowsersComparisonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB027182C26BC29009EA190 /* BrowsersComparisonModel.swift */; }; 9FB0271B2C2927D0009EA190 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB0271A2C2927D0009EA190 /* OnboardingView.swift */; }; 9FB0271D2C293619009EA190 /* OnboardingIntroViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */; }; - 9FB893F82C784A1700332E5E /* Onboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 9FB893F72C784A1700332E5E /* Onboarding */; }; 9FCFCD802C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */; }; 9FCFCD812C6B020D006EB7A0 /* LaunchOptionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */; }; + 9FCFCD852C75C91A006EB7A0 /* ProgressBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */; }; + 9FDEC7B42C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7B32C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift */; }; + 9FDEC7B62C8FDFD600C7A692 /* OnboardingManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7B52C8FDFD600C7A692 /* OnboardingManagerMock.swift */; }; + 9FDEC7B82C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7B72C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift */; }; + 9FDEC7BA2C9006E000C7A692 /* BrowserComparisonModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7B92C9006E000C7A692 /* BrowserComparisonModelTests.swift */; }; + 9FDEC7BC2C91204900C7A692 /* AppIconPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7BB2C91204900C7A692 /* AppIconPickerViewModel.swift */; }; + 9FDEC7BF2C91264C00C7A692 /* OnboardingAddressBarPositionPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7BE2C91264C00C7A692 /* OnboardingAddressBarPositionPicker.swift */; }; + 9FDEC7C12C9127F100C7A692 /* OnboardingAddressBarPositionPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEC7C02C9127F100C7A692 /* OnboardingAddressBarPositionPickerViewModel.swift */; }; 9FE05CEE2C36424E00D9046B /* OnboardingPixelReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE05CED2C36424E00D9046B /* OnboardingPixelReporter.swift */; }; 9FE05CF12C36468A00D9046B /* OnboardingPixelReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE05CEF2C3642F900D9046B /* OnboardingPixelReporterTests.swift */; }; 9FE08BD32C2A5B88001D5EBC /* OnboardingTextStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE08BD22C2A5B88001D5EBC /* OnboardingTextStyles.swift */; }; @@ -849,6 +868,11 @@ C185ED672BD43DA100BAE9DC /* ImportPasswordsStatusHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C185ED632BD438AF00BAE9DC /* ImportPasswordsStatusHandlerTests.swift */; }; C18ED43A2AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18ED4392AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift */; }; C18ED43C2AB8364400BF3805 /* FileTextPreviewDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18ED43B2AB8364400BF3805 /* FileTextPreviewDebugViewController.swift */; }; + C1935A0E2C88D11D001AD72D /* AutofillSurveyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1935A0D2C88D11D001AD72D /* AutofillSurveyView.swift */; }; + C1935A102C88D131001AD72D /* AutofillSurveyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1935A0F2C88D131001AD72D /* AutofillSurveyManager.swift */; }; + C1935A122C88D1D8001AD72D /* AutofillHeaderViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1935A112C88D1D8001AD72D /* AutofillHeaderViewFactory.swift */; }; + C1935A222C89CA9F001AD72D /* AutofillSurveyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1935A212C89CA9F001AD72D /* AutofillSurveyManagerTests.swift */; }; + C1935A242C89CC6D001AD72D /* AutofillHeaderViewFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1935A232C89CC6D001AD72D /* AutofillHeaderViewFactoryTests.swift */; }; C1963863283794A000298D4D /* BookmarksCachingSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1963862283794A000298D4D /* BookmarksCachingSearch.swift */; }; C1B7B51C28941E980098FD6A /* HomeMessageViewModelBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B7B51B28941E980098FD6A /* HomeMessageViewModelBuilder.swift */; }; C1B7B52528941F2A0098FD6A /* RemoteMessagingClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B7B52128941F2A0098FD6A /* RemoteMessagingClient.swift */; }; @@ -980,6 +1004,7 @@ EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE01EB3F2AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift */; }; EE01EB432AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */; }; EE0798C52B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */; }; + EE0D1B9C2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0D1B9B2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift */; }; EE3766DE2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */; }; EE3B226B29DE0F110082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; }; EE3B226C29DE0FD30082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; }; @@ -1045,7 +1070,6 @@ F15531922BF215ED0029ED04 /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F15531912BF215ED0029ED04 /* SubscriptionTestingUtilities */; }; F15531942BF215F60029ED04 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F15531932BF215F60029ED04 /* Subscription */; }; F15531962BF215F60029ED04 /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F15531952BF215F60029ED04 /* SubscriptionTestingUtilities */; }; - F1564F032B7B915F00D454A6 /* AppDelegate+Attribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1564F022B7B915F00D454A6 /* AppDelegate+Attribution.swift */; }; F15D43201E706CC500BF2CDC /* AutocompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15D431F1E706CC500BF2CDC /* AutocompleteViewController.swift */; }; F1617C131E572E0300DEDCAF /* TabSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C121E572E0300DEDCAF /* TabSwitcherViewController.swift */; }; F1617C151E57336D00DEDCAF /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1617C141E57336D00DEDCAF /* TabManager.swift */; }; @@ -1413,11 +1437,13 @@ 31584619281B08F5004ADB8B /* AutofillLoginListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginListViewModel.swift; sourceTree = ""; }; 3161D13127AC161B00285CF6 /* DownloadMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadMetadata.swift; sourceTree = ""; }; 31669B9928020A460071CC18 /* SaveLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveLoginViewModel.swift; sourceTree = ""; }; + 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackManagerTests.swift; sourceTree = ""; }; 316931D627BD10BB0095F5ED /* SaveToDownloadsAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToDownloadsAlert.swift; sourceTree = ""; }; 316931D827BD22A80095F5ED /* DownloadActionMessageViewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadActionMessageViewHelper.swift; sourceTree = ""; }; 3170048127A9504F00C03F35 /* DownloadMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadMocks.swift; sourceTree = ""; }; 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillInterfaceEmailTruncatorTests.swift; sourceTree = ""; }; 31794BFF2821DFB600F18633 /* DuckUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DuckUI; sourceTree = ""; }; + 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackStorage.swift; sourceTree = ""; }; 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerStorage.swift; sourceTree = ""; }; 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginDetailsHeaderView.swift; sourceTree = ""; }; 319A370F28299A850079FBCE /* PasswordHider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordHider.swift; sourceTree = ""; }; @@ -1426,6 +1452,9 @@ 31A42563285A09E800049386 /* FaviconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconView.swift; sourceTree = ""; }; 31A42565285A0A6300049386 /* FaviconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconViewModel.swift; sourceTree = ""; }; 31B1FA86286EFC5C00CA3C1C /* XCTestCaseExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtension.swift; sourceTree = ""; }; + 31B2F10E2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackManager.swift; sourceTree = ""; }; + 31B2F1102C92FEE000CD30E3 /* MarketplaceAdPostback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostback.swift; sourceTree = ""; }; + 31B2F1122C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackUpdater.swift; sourceTree = ""; }; 31B2F11E287846320040427A /* NoMicPermissionAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoMicPermissionAlert.swift; sourceTree = ""; }; 31B524562715BB23002225AB /* WebJSAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebJSAlert.swift; sourceTree = ""; }; 31BC5F402C2B0B540004DF37 /* DuckPlayer.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DuckPlayer.xcassets; sourceTree = ""; }; @@ -1850,6 +1879,7 @@ 981685572521EEF600FA91A1 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/MainInterface.strings; sourceTree = ""; }; 981685A825221ACF00FA91A1 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nb; path = nb.lproj/Localizable.stringsdict; sourceTree = ""; }; 9817C9C221EF594700884F65 /* AutoClear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoClear.swift; sourceTree = ""; }; + 981C49AF2C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreIdManagerTests.swift; sourceTree = ""; }; 981CA7E92617797500E119D5 /* MainViewController+AddFavoriteFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewController+AddFavoriteFlow.swift"; sourceTree = ""; }; 981DCA922521EFAB00CD4C18 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = ""; }; 981DCA932521EFAB00CD4C18 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2469,10 +2499,15 @@ 9F69331C2C5A191400CD6A5D /* MockTutorialSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTutorialSettings.swift; sourceTree = ""; }; 9F69331E2C5B1D0C00CD6A5D /* OnFirstAppearViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnFirstAppearViewModifier.swift; sourceTree = ""; }; 9F6933202C5B9A5B00CD6A5D /* OnboardingHostingControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHostingControllerMock.swift; sourceTree = ""; }; + 9F7CFF752C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AppIconPickerContent.swift"; sourceTree = ""; }; 9F7CFF772C86E3E10012833E /* OnboardingManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManagerTests.swift; sourceTree = ""; }; + 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPickerViewModelTests.swift; sourceTree = ""; }; + 9F7CFF7E2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+AddressBarPositionContent.swift"; sourceTree = ""; }; 9F8007252C5261AF003EDAF4 /* MockPrivacyDataReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyDataReporter.swift; sourceTree = ""; }; + 9F96F73E2C914C57009E45D5 /* OnboardingGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingGradient.swift; sourceTree = ""; }; 9F9A922D2C86A56B001D036D /* OnboardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManager.swift; sourceTree = ""; }; 9F9A92302C86AAE9001D036D /* OnboardingDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingDebugView.swift; sourceTree = ""; }; + 9F9A92332C86B42B001D036D /* AppIconPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPicker.swift; sourceTree = ""; }; 9F9EE4CC2C377D3F00D4118E /* OnboardingFirePixelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFirePixelMock.swift; sourceTree = ""; }; 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+Landing.swift"; sourceTree = ""; }; 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionContainerViewFactory.swift; sourceTree = ""; }; @@ -2483,6 +2518,14 @@ 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingIntroViewModel.swift; sourceTree = ""; }; 9FCFCD7D2C6AF52A006EB7A0 /* LaunchOptionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LaunchOptionsHandler.swift; path = ../DuckDuckGo/LaunchOptionsHandler.swift; sourceTree = ""; }; 9FCFCD7F2C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchOptionsHandlerTests.swift; sourceTree = ""; }; + 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.swift; sourceTree = ""; }; + 9FDEC7B32C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAddressBarPositionPickerViewModelTests.swift; sourceTree = ""; }; + 9FDEC7B52C8FDFD600C7A692 /* OnboardingManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManagerMock.swift; sourceTree = ""; }; + 9FDEC7B72C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingIntroViewModel+Copy.swift"; sourceTree = ""; }; + 9FDEC7B92C9006E000C7A692 /* BrowserComparisonModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserComparisonModelTests.swift; sourceTree = ""; }; + 9FDEC7BB2C91204900C7A692 /* AppIconPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPickerViewModel.swift; sourceTree = ""; }; + 9FDEC7BE2C91264C00C7A692 /* OnboardingAddressBarPositionPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAddressBarPositionPicker.swift; sourceTree = ""; }; + 9FDEC7C02C9127F100C7A692 /* OnboardingAddressBarPositionPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAddressBarPositionPickerViewModel.swift; sourceTree = ""; }; 9FE05CED2C36424E00D9046B /* OnboardingPixelReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPixelReporter.swift; sourceTree = ""; }; 9FE05CEF2C3642F900D9046B /* OnboardingPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPixelReporterTests.swift; sourceTree = ""; }; 9FE08BD22C2A5B88001D5EBC /* OnboardingTextStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTextStyles.swift; sourceTree = ""; }; @@ -2617,6 +2660,11 @@ C185ED652BD43A5500BAE9DC /* MockDDGSyncing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDDGSyncing.swift; sourceTree = ""; }; C18ED4392AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillSettingsEnableFooterView.swift; sourceTree = ""; }; C18ED43B2AB8364400BF3805 /* FileTextPreviewDebugViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileTextPreviewDebugViewController.swift; sourceTree = ""; }; + C1935A0D2C88D11D001AD72D /* AutofillSurveyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillSurveyView.swift; sourceTree = ""; }; + C1935A0F2C88D131001AD72D /* AutofillSurveyManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillSurveyManager.swift; sourceTree = ""; }; + C1935A112C88D1D8001AD72D /* AutofillHeaderViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillHeaderViewFactory.swift; sourceTree = ""; }; + C1935A212C89CA9F001AD72D /* AutofillSurveyManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillSurveyManagerTests.swift; sourceTree = ""; }; + C1935A232C89CC6D001AD72D /* AutofillHeaderViewFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillHeaderViewFactoryTests.swift; sourceTree = ""; }; C1963862283794A000298D4D /* BookmarksCachingSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksCachingSearch.swift; sourceTree = ""; }; C1B0F6412AB08BE9001EAF05 /* MockPrivacyConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyConfiguration.swift; sourceTree = ""; }; C1B7B51B28941E980098FD6A /* HomeMessageViewModelBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeMessageViewModelBuilder.swift; sourceTree = ""; }; @@ -2762,6 +2810,7 @@ EE01EB3F2AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNSettingsViewModel.swift; sourceTree = ""; }; EE01EB422AFC1E0A0096AAC9 /* NetworkProtectionVPNLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNLocationView.swift; sourceTree = ""; }; EE0798C42B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; + EE0D1B9B2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubAutofillLoginImportStateProvider.swift; sourceTree = ""; }; EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationPresenter.swift; sourceTree = ""; }; EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockInternalUserStoring.swift; sourceTree = ""; }; EE3B98EA2A9634CC002F63A0 /* DuckDuckGoAlpha.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoAlpha.entitlements; sourceTree = ""; }; @@ -2851,7 +2900,6 @@ F143C32C1E4A9A4800CFDE3A /* UIViewControllerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIViewControllerExtension.swift; path = ../Core/UIViewControllerExtension.swift; sourceTree = ""; }; F143C3451E4AA32D00CFDE3A /* SearchBarExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SearchBarExtension.swift; path = ../Core/SearchBarExtension.swift; sourceTree = ""; }; F14E491E1E391CE900DC037C /* URLExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLExtensionTests.swift; sourceTree = ""; }; - F1564F022B7B915F00D454A6 /* AppDelegate+Attribution.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Attribution.swift"; sourceTree = ""; }; F159BDA31F0BDB5A00B4A01D /* TabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewController.swift; sourceTree = ""; }; F15D431F1E706CC500BF2CDC /* AutocompleteViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteViewController.swift; sourceTree = ""; }; F1617C121E572E0300DEDCAF /* TabSwitcherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabSwitcherViewController.swift; sourceTree = ""; }; @@ -2979,7 +3027,7 @@ F42D541D29DCA40B004C4FF1 /* DesignResourcesKit in Frameworks */, 85875B6129912A9900115F05 /* SyncUI in Frameworks */, F4D7F634298C00C3006C3AE9 /* FindInPageIOSJSSupport in Frameworks */, - 9FB893F82C784A1700332E5E /* Onboarding in Frameworks */, + 9F96F73B2C9144D5009E45D5 /* Onboarding in Frameworks */, 85D598872927F84C00FA3B1B /* Crashes in Frameworks */, D664C7DD2B28A02800CBFA76 /* StoreKit.framework in Frameworks */, ); @@ -3474,6 +3522,14 @@ name = LoginDetails; sourceTree = ""; }; + 316790E32C9350980090B0A2 /* MarketplaceAdPostback */ = { + isa = PBXGroup; + children = ( + 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */, + ); + name = MarketplaceAdPostback; + sourceTree = ""; + }; 316931DA27BD24B60095F5ED /* Alerts */ = { isa = PBXGroup; children = ( @@ -3507,6 +3563,7 @@ 319A37132829A5450079FBCE /* Table */ = { isa = PBXGroup; children = ( + C1935A0C2C88D101001AD72D /* Survey */, 31CC224828369B38001654A4 /* AutofillLoginSettingsListViewController.swift */, 319A37142829A55F0079FBCE /* AutofillListItemTableViewCell.swift */, 310ECFDC282A8BB0005029B3 /* EnableAutofillSettingsTableViewCell.swift */, @@ -3518,10 +3575,22 @@ C1B924B62ACD6E6800EE7B06 /* AutofillNeverSavedTableViewCell.swift */, C1836CE02C359EC90016D057 /* AutofillBreakageReportCellContentView.swift */, C1836CE42C35A0EA0016D057 /* AutofillBreakageReportTableViewCell.swift */, + C1935A112C88D1D8001AD72D /* AutofillHeaderViewFactory.swift */, ); name = Table; sourceTree = ""; }; + 31B2F10D2C92FEB000CD30E3 /* MarketplaceAdPostback */ = { + isa = PBXGroup; + children = ( + 31B2F10E2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift */, + 31B2F1122C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift */, + 31B2F1102C92FEE000CD30E3 /* MarketplaceAdPostback.swift */, + 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */, + ); + name = MarketplaceAdPostback; + sourceTree = ""; + }; 31C138A127A334F600FFD4B2 /* Downloads */ = { isa = PBXGroup; children = ( @@ -4640,10 +4709,13 @@ children = ( 9FE08BDB2C2A88FA001D5EBC /* OnboardingIntroViewController.swift */, 9FB0271C2C293619009EA190 /* OnboardingIntroViewModel.swift */, + 9FDEC7B72C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift */, 9FB0271A2C2927D0009EA190 /* OnboardingView.swift */, 9F9EE4D32C37BB1300D4118E /* OnboardingView+Landing.swift */, 9FB027112C2526DD009EA190 /* OnboardingView+IntroDialogContent.swift */, 9FB027132C252E0C009EA190 /* OnboardingView+BrowsersComparisonContent.swift */, + 9F7CFF752C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift */, + 9F7CFF7E2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift */, ); path = OnboardingIntro; sourceTree = ""; @@ -4662,6 +4734,9 @@ 9F4CC51E2C48D758006A96EB /* ContextualDaxDialogsFactoryTests.swift */, 9F69331A2C5A16E200CD6A5D /* OnboardingDaxFavouritesTests.swift */, 9F7CFF772C86E3E10012833E /* OnboardingManagerTests.swift */, + 9F7CFF7C2C89B69A0012833E /* AppIconPickerViewModelTests.swift */, + 9FDEC7B32C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift */, + 9FDEC7B92C9006E000C7A692 /* BrowserComparisonModelTests.swift */, ); name = Onboarding; sourceTree = ""; @@ -4683,6 +4758,15 @@ path = ContextualOnboarding; sourceTree = ""; }; + 9F96F73D2C914C3D009E45D5 /* Background */ = { + isa = PBXGroup; + children = ( + 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */, + 9F96F73E2C914C57009E45D5 /* OnboardingGradient.swift */, + ); + path = Background; + sourceTree = ""; + }; 9F9A922C2C86A560001D036D /* Manager */ = { isa = PBXGroup; children = ( @@ -4699,6 +4783,15 @@ name = OnboardingDebugView; sourceTree = ""; }; + 9F9A92322C86B419001D036D /* AppIconPicker */ = { + isa = PBXGroup; + children = ( + 9F9A92332C86B42B001D036D /* AppIconPicker.swift */, + 9FDEC7BB2C91204900C7A692 /* AppIconPickerViewModel.swift */, + ); + path = AppIconPicker; + sourceTree = ""; + }; 9F9EE4CB2C377D2400D4118E /* Mocks */ = { isa = PBXGroup; children = ( @@ -4706,6 +4799,7 @@ 9F4CC5142C47AD08006A96EB /* ContextualOnboardingPresenterMock.swift */, 9F6933182C59BB0300CD6A5D /* OnboardingPixelReporterMock.swift */, 9F6933202C5B9A5B00CD6A5D /* OnboardingHostingControllerMock.swift */, + 9FDEC7B52C8FDFD600C7A692 /* OnboardingManagerMock.swift */, ); name = Mocks; sourceTree = ""; @@ -4719,6 +4813,15 @@ path = BrowsersComparison; sourceTree = ""; }; + 9FDEC7BD2C9125EC00C7A692 /* AddressBarPositionPicker */ = { + isa = PBXGroup; + children = ( + 9FDEC7BE2C91264C00C7A692 /* OnboardingAddressBarPositionPicker.swift */, + 9FDEC7C02C9127F100C7A692 /* OnboardingAddressBarPositionPickerViewModel.swift */, + ); + path = AddressBarPositionPicker; + sourceTree = ""; + }; 9FE05CEC2C36423C00D9046B /* Pixels */ = { isa = PBXGroup; children = ( @@ -4747,6 +4850,9 @@ 9FF7E9802C22A19800902BE5 /* OnboardingExperiment */ = { isa = PBXGroup; children = ( + 9F96F73D2C914C3D009E45D5 /* Background */, + 9FDEC7BD2C9125EC00C7A692 /* AddressBarPositionPicker */, + 9F9A92322C86B419001D036D /* AppIconPicker */, 9F9A922C2C86A560001D036D /* Manager */, 9FE05CEC2C36423C00D9046B /* Pixels */, 56D060202C356B0B003BAEB5 /* ContextualDaxDialogs */, @@ -4755,7 +4861,7 @@ 9FB027172C26BC0F009EA190 /* BrowsersComparison */, 9F23B7FF2C2BABE000950875 /* OnboardingIntro */, 9F5E5AAA2C3D0FAA00165F54 /* ContextualOnboarding */, - 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */, + 9FCFCD842C75C91A006EB7A0 /* ProgressBarView.swift */, ); path = OnboardingExperiment; sourceTree = ""; @@ -4975,6 +5081,49 @@ name = Import; sourceTree = ""; }; + C1935A0C2C88D101001AD72D /* Survey */ = { + isa = PBXGroup; + children = ( + C1935A0F2C88D131001AD72D /* AutofillSurveyManager.swift */, + C1935A0D2C88D11D001AD72D /* AutofillSurveyView.swift */, + ); + name = Survey; + sourceTree = ""; + }; + C1935A1D2C89CA4B001AD72D /* Management */ = { + isa = PBXGroup; + children = ( + C1935A1E2C89CA53001AD72D /* List */, + F40F843528C938370081AE75 /* AutofillLoginListViewModelTests.swift */, + ); + name = Management; + sourceTree = ""; + }; + C1935A1E2C89CA53001AD72D /* List */ = { + isa = PBXGroup; + children = ( + C1935A1F2C89CA5A001AD72D /* Table */, + ); + name = List; + sourceTree = ""; + }; + C1935A1F2C89CA5A001AD72D /* Table */ = { + isa = PBXGroup; + children = ( + C1935A202C89CA5F001AD72D /* Survey */, + C1935A232C89CC6D001AD72D /* AutofillHeaderViewFactoryTests.swift */, + ); + name = Table; + sourceTree = ""; + }; + C1935A202C89CA5F001AD72D /* Survey */ = { + isa = PBXGroup; + children = ( + C1935A212C89CA9F001AD72D /* AutofillSurveyManagerTests.swift */, + ); + name = Survey; + sourceTree = ""; + }; C1AFFC4B2B8773060060448E /* AuthConfirmation */ = { isa = PBXGroup; children = ( @@ -5652,6 +5801,7 @@ F143C2E51E4A4CD400CFDE3A /* Core */ = { isa = PBXGroup; children = ( + 31B2F10D2C92FEB000CD30E3 /* MarketplaceAdPostback */, F1CE42A71ECA0A520074A8DF /* Bookmarks */, 837774491F8E1ECE00E17A29 /* ContentBlocker */, F143C2E61E4A4CD400CFDE3A /* Core.h */, @@ -5868,6 +6018,7 @@ 834DF990248FDDF60075EA48 /* UserAgentTests.swift */, 8540BD5123D8C2220057FDD2 /* PreserveLoginsTests.swift */, 850559D123CF710C0055C0D5 /* WebCacheManagerTests.swift */, + 981C49AF2C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift */, F198D7971E3A45D90088DA8A /* WKWebViewConfigurationExtensionTests.swift */, 85AD49ED2B6149110085D2D1 /* CookieStorageTests.swift */, ); @@ -5974,7 +6125,6 @@ CB24F70E29A3EB15006DCC58 /* AppConfigurationURLProvider.swift */, 84E341951E2F7EFB00BDBA6F /* AppDelegate.swift */, 85DB12EC2A1FED0C000A4A72 /* AppDelegate+AppDeepLinks.swift */, - F1564F022B7B915F00D454A6 /* AppDelegate+Attribution.swift */, 98B31291218CCB8C00E54DE1 /* AppDependencyProvider.swift */, 85BA58591F3506AE00C6E8CA /* AppSettings.swift */, 85BA58541F34F49E00C6E8CA /* AppUserDefaults.swift */, @@ -6119,6 +6269,7 @@ F1E092B31E92A6B900732CCC /* Core */ = { isa = PBXGroup; children = ( + 316790E32C9350980090B0A2 /* MarketplaceAdPostback */, 858479CA2B8795BF00D156C1 /* History */, EA7EFE662677F5BD0075464E /* PrivacyReferenceTests */, 83EDCC3E1F86B363005CDFCD /* API */, @@ -6159,9 +6310,9 @@ F40F843228C92B1C0081AE75 /* Autofill */ = { isa = PBXGroup; children = ( + C1935A1D2C89CA4B001AD72D /* Management */, C185ED622BD4388F00BAE9DC /* Import */, C1BF0BA629B63E0400482B73 /* AutofillLoginUI */, - F40F843528C938370081AE75 /* AutofillLoginListViewModelTests.swift */, C1D21E2E293A599C006E5A05 /* AutofillLoginSessionTests.swift */, C1CDA31D2AFBF811006D1476 /* AutofillNeverPromptWebsitesManagerTests.swift */, ); @@ -6186,6 +6337,7 @@ 31951E9328230D8900CAF535 /* Shared */, F407605428131923006B1E0B /* SaveLogin */, EE5929612C5A8AF40029380B /* AutofillUsageMonitor.swift */, + EE0D1B9B2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift */, ); name = Autofill; sourceTree = ""; @@ -6331,7 +6483,7 @@ CB941A6D2B96AB08000F9E7A /* PrivacyDashboard */, F1D43AF92B99C1D300BAB743 /* BareBonesBrowserKit */, 9F8FE9482BAE50E50071E372 /* Lottie */, - 9FB893F72C784A1700332E5E /* Onboarding */, + 9F96F73A2C9144D5009E45D5 /* Onboarding */, ); productName = DuckDuckGo; productReference = 84E341921E2F7EFB00BDBA6F /* DuckDuckGo.app */; @@ -7105,6 +7257,7 @@ BDE91CDE2C62B90F0005CB74 /* UnifiedFeedbackRootView.swift in Sources */, D65625A12C232F5E006EF297 /* SettingsDuckPlayerView.swift in Sources */, D6FEB8B52B74994000C3615F /* HeadlessWebViewCoordinator.swift in Sources */, + 9F96F73F2C914C57009E45D5 /* OnboardingGradient.swift in Sources */, 6FE1273D2C204C2500EB5724 /* FavoritesView.swift in Sources */, 8528AE81212F15D600D0BD74 /* AppRatingPrompt.xcdatamodeld in Sources */, 1E24295E293F57FA00584836 /* LottieView.swift in Sources */, @@ -7205,7 +7358,6 @@ 9F5E5AB02C3E4C6000165F54 /* ContextualOnboardingPresenter.swift in Sources */, 310D091B2799F54900DC0060 /* DownloadManager.swift in Sources */, 98D98A7425ED88D100D8E3DF /* BrowsingMenuEntryViewCell.swift in Sources */, - F1564F032B7B915F00D454A6 /* AppDelegate+Attribution.swift in Sources */, 98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */, 4B2C79612C5B27AC00A240CC /* VPNSnoozeActivityAttributes.swift in Sources */, CB9B873C278C8FEA001F4906 /* WidgetEducationView.swift in Sources */, @@ -7266,6 +7418,7 @@ 986DA94A24884B18004A7E39 /* WebViewTransition.swift in Sources */, 31B524572715BB23002225AB /* WebJSAlert.swift in Sources */, C1641EB32BC2F53C0012607A /* ImportPasswordsViewModel.swift in Sources */, + 9FDEC7BF2C91264C00C7A692 /* OnboardingAddressBarPositionPicker.swift in Sources */, 8536A1FD2ACF114B003AC5BA /* Theme+DesignSystem.swift in Sources */, F114C55B1E66EB020018F95F /* NibLoading.swift in Sources */, D6BFCB612B7525160051FF81 /* SubscriptionPIRViewModel.swift in Sources */, @@ -7278,6 +7431,7 @@ 37FCAABC2992F592000E420A /* MultilineScrollableTextFix.swift in Sources */, 85DFEDED24C7CCA500973FE7 /* AppWidthObserver.swift in Sources */, 4B6484F327FD1E350050A7A1 /* MenuControllerView.swift in Sources */, + 9F9A92342C86B42B001D036D /* AppIconPicker.swift in Sources */, CB825C962C071C9300BCC586 /* AlertViewPresenter.swift in Sources */, 1EE7C299294227EC0026C8CB /* AutoconsentSettingsViewController.swift in Sources */, 1E8AD1D527C2E22900ABA377 /* DownloadsListSectionViewModel.swift in Sources */, @@ -7292,6 +7446,7 @@ 1E8AD1C927BFAD1500ABA377 /* DirectoryMonitor.swift in Sources */, 377D80222AB48554002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */, 1E8AD1D127C000AB00ABA377 /* OngoingDownloadRow.swift in Sources */, + C1935A0E2C88D11D001AD72D /* AutofillSurveyView.swift in Sources */, 1DEAADF02BA46E0700E25A97 /* PrivateSearchView.swift in Sources */, 85058366219AE9EA00ED4EDB /* HomePageConfiguration.swift in Sources */, 1E9529A12C4E748B006E80D4 /* UINavigationControllerExtension.swift in Sources */, @@ -7312,6 +7467,7 @@ C1641EAF2BC2F5140012607A /* ImportPasswordsViewController.swift in Sources */, D63FF8982C1B6A45006DE24D /* DuckPlayer.swift in Sources */, 85B9CB8921AEBDD5009001F1 /* FavoriteHomeCell.swift in Sources */, + C1935A102C88D131001AD72D /* AutofillSurveyManager.swift in Sources */, 98999D5922FDA41500CBBE1B /* BasicAuthenticationAlert.swift in Sources */, C13B32D22A0E750700A59236 /* AutofillSettingStatus.swift in Sources */, 1DDF40202BA049FA006850D9 /* SettingsRootView.swift in Sources */, @@ -7367,6 +7523,7 @@ F1617C151E57336D00DEDCAF /* TabManager.swift in Sources */, 85449EF523FDA02800512AAF /* KeyboardSettingsViewController.swift in Sources */, 85C11E4C2090888C00BFFEB4 /* HomeRowReminder.swift in Sources */, + C1935A122C88D1D8001AD72D /* AutofillHeaderViewFactory.swift in Sources */, 31B2F11F287846320040427A /* NoMicPermissionAlert.swift in Sources */, 310C4B45281B5A9A00BA79A9 /* AutofillLoginDetailsView.swift in Sources */, 6F9FFE2D2C57AE8F00A238BE /* NewTabPageShortcutsSettingsModel.swift in Sources */, @@ -7378,6 +7535,7 @@ 9F9A92312C86AAE9001D036D /* OnboardingDebugView.swift in Sources */, 1EFDCBC127D2393C00916BC5 /* DownloadsDeleteHelper.swift in Sources */, C1836CE12C359EC90016D057 /* AutofillBreakageReportCellContentView.swift in Sources */, + 9FDEC7C12C9127F100C7A692 /* OnboardingAddressBarPositionPickerViewModel.swift in Sources */, 6F9FFE282C579DEA00A238BE /* NewTabPageSectionsSettingsStorage.swift in Sources */, 85374D3C21AC41E700FF5A1E /* FavoritesHomeViewSectionRenderer.swift in Sources */, 85DFEDF124C7EEA400973FE7 /* LargeOmniBarState.swift in Sources */, @@ -7385,6 +7543,7 @@ D6E83C602B22B3C9006C8AFB /* SettingsState.swift in Sources */, D6E83C482B20C812006C8AFB /* SettingsHostingController.swift in Sources */, F46FEC5727987A5F0061D9DF /* KeychainItemsDebugViewController.swift in Sources */, + 9F7CFF762C86BB8F0012833E /* OnboardingView+AppIconPickerContent.swift in Sources */, D68A21442B7EC08500BB372E /* SubscriptionExternalLinkView.swift in Sources */, BDD3B3552B8EF8DB005857A8 /* NetworkProtectionUNNotificationPresenter.swift in Sources */, BD862E0B2B30F9300073E2EE /* VPNFeedbackFormView.swift in Sources */, @@ -7485,6 +7644,7 @@ 1DEAADFF2BA7832F00E25A97 /* EmailProtectionView.swift in Sources */, 988F3DD3237DE8D900AEE34C /* ForgetDataAlert.swift in Sources */, D6FEB8B12B7498A300C3615F /* HeadlessWebView.swift in Sources */, + 9FDEC7BC2C91204900C7A692 /* AppIconPickerViewModel.swift in Sources */, F1FDC9352BF51E41006B1435 /* VPNSettings+Environment.swift in Sources */, 850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */, 6FB2A6802C2EA950004D20C8 /* FavoritesDefaultModel.swift in Sources */, @@ -7548,11 +7708,13 @@ 0283A2012C6E46E300508FBD /* BrokenSitePromptLimiter.swift in Sources */, 85DFEDF924CF3D0E00973FE7 /* TabsBarCell.swift in Sources */, 851672D32BED23FE00592F24 /* AutocompleteViewModel.swift in Sources */, + EE0D1B9C2C8B41DB00AC0987 /* StubAutofillLoginImportStateProvider.swift in Sources */, 8562CE152B9B645C00E1D399 /* CachedBookmarkSuggestions.swift in Sources */, C13F3F682B7F88100083BE40 /* AuthConfirmationPromptView.swift in Sources */, F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */, 4B5C462A2AF2A6E6002A4432 /* VPNIntents.swift in Sources */, 310742A62848CD780012660B /* BackForwardMenuHistoryItem.swift in Sources */, + 9FDEC7B82C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift in Sources */, 858566FB252E55D6007501B8 /* ImageCacheDebugViewController.swift in Sources */, D6E0C1832B7A2B1E00D5E1E9 /* DesktopDownloadView.swift in Sources */, 1E7A71172934EB6400B7EA19 /* OmniBarNotificationAnimator.swift in Sources */, @@ -7578,6 +7740,7 @@ 8505836A219F424500ED4EDB /* UIAlertControllerExtension.swift in Sources */, C12726F22A5FF8CB00215B02 /* EmailSignupPromptViewController.swift in Sources */, 983EABB8236198F6003948D1 /* DatabaseMigration.swift in Sources */, + 9FCFCD852C75C91A006EB7A0 /* ProgressBarView.swift in Sources */, 6F3537A42C4AC140009F8717 /* NewTabPageDaxLogoView.swift in Sources */, 314C92B827C3DD660042EC96 /* QuickLookPreviewView.swift in Sources */, 6F5345AF2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift in Sources */, @@ -7617,6 +7780,7 @@ 8590CB67268A2E520089F6BF /* RootDebugViewController.swift in Sources */, 1DEAADEA2BA4539800E25A97 /* SettingsAppearanceView.swift in Sources */, B623C1C22862CA9E0043013E /* DownloadSession.swift in Sources */, + 9F7CFF7F2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift in Sources */, F16390821E648B7A005B4550 /* HomeViewController.swift in Sources */, 985892522260B1B200EEB31B /* ProgressView.swift in Sources */, 85BA585A1F3506AE00C6E8CA /* AppSettings.swift in Sources */, @@ -7690,6 +7854,7 @@ C14882E327F20D9A00D59F0C /* BookmarksExporterTests.swift in Sources */, 85C29708247BDD060063A335 /* DaxDialogsBrowsingSpecTests.swift in Sources */, 9FE05CF12C36468A00D9046B /* OnboardingPixelReporterTests.swift in Sources */, + 9FDEC7B42C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift in Sources */, 85BA58581F34F72F00C6E8CA /* AppUserDefaultsTests.swift in Sources */, F1134EBC1F40D45700B73467 /* MockStatisticsStore.swift in Sources */, 9FCFCD802C6AF56D006EB7A0 /* LaunchOptionsHandlerTests.swift in Sources */, @@ -7705,6 +7870,7 @@ CBDD5DDF29A6736A00832877 /* APIHeadersTests.swift in Sources */, D6F557BA2C8859040034444B /* DuckPlayerExperimentTests.swift in Sources */, 986B45D0299E30A50089D2D7 /* BookmarkEntityTests.swift in Sources */, + 981C49B02C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift in Sources */, B6AD9E3828D4512E0019CDE9 /* EmbeddedTrackerDataTests.swift in Sources */, 6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */, 9F4CC51B2C48C0C7006A96EB /* MockTabDelegate.swift in Sources */, @@ -7732,7 +7898,9 @@ 1E1D8B5D2994FFE100C96994 /* AutoconsentMessageProtocolTests.swift in Sources */, 85C11E532090B23A00BFFEB4 /* UserDefaultsHomeRowReminderStorageTests.swift in Sources */, F1DA2F7D1EBCF23700313F51 /* ExternalUrlSchemeTests.swift in Sources */, + 9FDEC7B62C8FDFD600C7A692 /* OnboardingManagerMock.swift in Sources */, F198D78E1E39762C0088DA8A /* StringExtensionTests.swift in Sources */, + 9FDEC7BA2C9006E000C7A692 /* BrowserComparisonModelTests.swift in Sources */, 31B1FA87286EFC5C00CA3C1C /* XCTestCaseExtension.swift in Sources */, D62EC3BC2C2470E000FC9D04 /* DuckPlayerTests.swift in Sources */, 1E8146AE28C8ABF400D1AF63 /* PrivacyIconLogicTests.swift in Sources */, @@ -7745,6 +7913,7 @@ 8598F67B2405EB8D00FBC70C /* KeyboardSettingsTests.swift in Sources */, 98AAF8E4292EB46000DBDF06 /* BookmarksMigrationTests.swift in Sources */, 85D2187224BF24F2004373D2 /* NotFoundCachingDownloaderTests.swift in Sources */, + C1935A242C89CC6D001AD72D /* AutofillHeaderViewFactoryTests.swift in Sources */, C111B26927F579EF006558B1 /* BookmarkOrFolderTests.swift in Sources */, 6F7FB8E72C66197E00867DA7 /* NewTabPageSectionsSettingsModelTests.swift in Sources */, 851CD674244D7E6000331B98 /* UserDefaultsExtension.swift in Sources */, @@ -7764,6 +7933,7 @@ 983BD6B52B34760600AAC78E /* MockPrivacyConfiguration.swift in Sources */, 1E8146AD28C8ABF000D1AF63 /* TrackerAnimationLogicTests.swift in Sources */, C1CDA31E2AFBF811006D1476 /* AutofillNeverPromptWebsitesManagerTests.swift in Sources */, + 9F7CFF7D2C89B69A0012833E /* AppIconPickerViewModelTests.swift in Sources */, B6AD9E3A28D456820019CDE9 /* PrivacyConfigurationManagerMock.swift in Sources */, F189AED71F18F6DE001EBAE1 /* TabTests.swift in Sources */, 6FABAA692C6116FD003762EC /* NewTabPageShortcutsSettingsStorageTests.swift in Sources */, @@ -7777,6 +7947,7 @@ 4BC21A2F27238B7500229F0E /* RunLoopExtensionTests.swift in Sources */, 314A3EFC293905EC00D3D4C8 /* BrokenSiteReportingTests.swift in Sources */, 851B1283221FE65E004781BC /* ImproveOnboardingExperiment1Tests.swift in Sources */, + 316790E52C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift in Sources */, F194FAFB1F14E622009B4DF8 /* UIFontExtensionTests.swift in Sources */, BBFF18B12C76448100C48D7D /* QuerySubmittedTests.swift in Sources */, 854A8D812C7F4452001D62E5 /* AtbTests.swift in Sources */, @@ -7825,6 +7996,7 @@ CBCCF96828885DEE006F4A71 /* AppPrivacyConfigurationTests.swift in Sources */, 6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */, 310742AB2848E6FD0012660B /* BackForwardMenuHistoryItemURLSanitizerTests.swift in Sources */, + C1935A222C89CA9F001AD72D /* AutofillSurveyManagerTests.swift in Sources */, 22CB1ED8203DDD2C00D2C724 /* AppDeepLinksTests.swift in Sources */, 9847C00527A41A0A00DB07AA /* WebViewTestHelper.swift in Sources */, 3170048227A9504F00C03F35 /* DownloadMocks.swift in Sources */, @@ -7956,6 +8128,7 @@ 851624C22B95F8BD002D5CD7 /* HistoryCapture.swift in Sources */, 85CA53AC24BBD39300A6288C /* FaviconRequestModifier.swift in Sources */, CB258D1D29A52AF900DEBA24 /* EtagStorage.swift in Sources */, + 31B2F10F2C92FECC00CD30E3 /* MarketplaceAdPostbackManager.swift in Sources */, C1B7B52D2894469D0098FD6A /* DefaultVariantManager.swift in Sources */, 9833913727AC400800DAF119 /* AppTrackerDataSetProvider.swift in Sources */, 83004E802193BB8200DA013C /* WKNavigationExtension.swift in Sources */, @@ -7990,6 +8163,7 @@ 4B75EA9226A266CB00018634 /* PrintingUserScript.swift in Sources */, 37445F972A155F7C0029F789 /* SyncDataProviders.swift in Sources */, EE9D68DE2AE2A65600B55EF4 /* UserDefaults+NetworkProtection.swift in Sources */, + 31B2F1112C92FEE000CD30E3 /* MarketplaceAdPostback.swift in Sources */, CB258D1F29A52B2500DEBA24 /* Configuration.swift in Sources */, BDC234F72B27F51100D3C798 /* UniquePixel.swift in Sources */, 98629D312C21765A001E6031 /* BookmarksStateValidation.swift in Sources */, @@ -8014,6 +8188,7 @@ 9FE08BDA2C2A86D0001D5EBC /* URLOpener.swift in Sources */, 37DF000F29F9D635002B7D3E /* SyncBookmarksAdapter.swift in Sources */, B652DF10287C2C1600C12A9C /* ContentBlocking.swift in Sources */, + 31B2F1132C92FEF500CD30E3 /* MarketplaceAdPostbackUpdater.swift in Sources */, 4BE2756827304F57006B20B0 /* URLRequestExtension.swift in Sources */, 85BA79911F6FF75000F59015 /* ContentBlockerStoreConstants.swift in Sources */, 85E242172AB1B54D000F3E28 /* ReturnUserMeasurement.swift in Sources */, @@ -8040,6 +8215,7 @@ 85200FA11FBC5BB5001AF290 /* DDGPersistenceContainer.swift in Sources */, 9FEA22322C3270BD006B03BF /* TimerInterface.swift in Sources */, 1E4DCF4C27B6A4CB00961E25 /* URLFileExtension.swift in Sources */, + 317F5F982C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift in Sources */, EE50053029C3BA0800AE0773 /* InternalUserStore.swift in Sources */, F1D477CB1F2149C40031ED49 /* Type.swift in Sources */, 983C52E42C2C050B007B5747 /* BookmarksStateRepair.swift in Sources */, @@ -8924,7 +9100,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -8961,7 +9137,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9051,7 +9227,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9078,7 +9254,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9227,7 +9403,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9252,7 +9428,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9321,7 +9497,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9355,7 +9531,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9388,7 +9564,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9418,7 +9594,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9728,7 +9904,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9759,7 +9935,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9787,7 +9963,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9820,7 +9996,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9850,7 +10026,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9883,11 +10059,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10120,7 +10296,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10147,7 +10323,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10179,7 +10355,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10216,7 +10392,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10251,7 +10427,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10286,11 +10462,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10463,11 +10639,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10496,10 +10672,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 3; + DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10914,8 +11090,9 @@ package = 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */; productName = Lottie; }; - 9FB893F72C784A1700332E5E /* Onboarding */ = { + 9F96F73A2C9144D5009E45D5 /* Onboarding */ = { isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Onboarding; }; B6DFE6D52BC7E47F00A9CE59 /* SwiftLintTool */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f1ec86aaf1..81601403b8 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "b8c858a33cfe665ddfe177df4dbd63d12f6777a6", - "version" : "6.14.1" + "revision" : "9f3717b3913a12956f1386fe7b657f68545fba83", + "version" : "6.15.0" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "1f12b78d9bac4a1d9b6bad18dc2ef0593bed34a3", - "version" : "13.0.0" + "revision" : "1fee787458d13f8ed07f9fe81aecd6e59609339e", + "version" : "13.1.0" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "665b23dc656c9f787494620494f8e56098a900b2", - "version" : "5.1.1" + "revision" : "9de2b2aa317a48d3ee31116dc15b0feeb2cc9414", + "version" : "5.3.0" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme index aa78a92807..b9ef06c977 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme @@ -27,6 +27,7 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "NO" + language = "en" codeCoverageEnabled = "YES"> Bool { - // Attribution support - updateAttribution(conversionValue: 1) - #if targetEnvironment(simulator) if ProcessInfo.processInfo.environment["UITESTING"] == "true" { // Disable hardware keyboards. @@ -522,6 +521,7 @@ import os.log } AppConfigurationFetch().start { result in + self.sendAppLaunchPostback() if case .assetsUpdated(let protectionsUpdated) = result, protectionsUpdated { ContentBlocking.shared.contentBlockingManager.scheduleCompilation() } @@ -576,7 +576,7 @@ import os.log } func applicationWillResignActive(_ application: UIApplication) { - Task { + Task { @MainActor in await refreshShortcuts() await vpnWorkaround.removeRedditSessionWorkaround() } @@ -756,6 +756,14 @@ import os.log // MARK: private + private func sendAppLaunchPostback() { + // Attribution support + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + if privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .marketplaceAdPostback) { + marketplaceAdPostbackManager.sendAppLaunchPostback() + } + } + private func cleanUpATBAndAssignVariant(variantManager: VariantManager, daxDialogs: DaxDialogs) { let historyMessageManager = HistoryMessageManager() @@ -771,6 +779,9 @@ import os.log // New users don't see the message historyMessageManager.dismiss() + + // Setup storage for marketplace postback + marketplaceAdPostbackManager.updateReturningUserValue() } } @@ -967,7 +978,6 @@ import os.log UIApplication.shared.shortcutItems = nil } } - } extension AppDelegate: BlankSnapshotViewRecoveringDelegate { diff --git a/DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Contents.json new file mode 100644 index 0000000000..608aeb0457 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Passwords-DDG-96x96.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Passwords-DDG-96x96.svg b/DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Passwords-DDG-96x96.svg new file mode 100644 index 0000000000..2c92f4ff23 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Passwords-DDG-96x96.imageset/Passwords-DDG-96x96.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/AutofillDebugViewController.swift b/DuckDuckGo/AutofillDebugViewController.swift index 78ebb76778..dad15dbfa9 100644 --- a/DuckDuckGo/AutofillDebugViewController.swift +++ b/DuckDuckGo/AutofillDebugViewController.swift @@ -33,6 +33,7 @@ class AutofillDebugViewController: UITableViewController { case resetAutofillData = 204 case addAutofillData = 205 case resetAutofillBrokenReports = 206 + case resetAutofillSurveys = 207 } let defaults = AppUserDefaults() @@ -87,6 +88,11 @@ class AutofillDebugViewController: UITableViewController { let expiryDate = Calendar.current.date(byAdding: .day, value: 60, to: Date())! _ = reporter.persistencyManager.removeExpiredItems(currentDate: expiryDate) ActionMessageView.present(message: "Autofill Broken Reports reset") + } else if cell.tag == Row.resetAutofillSurveys.rawValue { + tableView.deselectRow(at: indexPath, animated: true) + let autofillSurveyManager = AutofillSurveyManager() + autofillSurveyManager.resetSurveys() + ActionMessageView.present(message: "Autofill Surveys reset") } } } @@ -114,7 +120,7 @@ class AutofillDebugViewController: UITableViewController { let secureVault = try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter()) for i in 1...count { - let account = SecureVaultModels.WebsiteAccount(title: "", username: "Dax \(i)", domain: "https://fill.dev", notes: "") + let account = SecureVaultModels.WebsiteAccount(title: "", username: "Dax \(i)", domain: "fill.dev", notes: "") let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: "password".data(using: .utf8)) do { _ = try secureVault?.storeWebsiteCredentials(credentials) diff --git a/DuckDuckGo/AutofillHeaderViewFactory.swift b/DuckDuckGo/AutofillHeaderViewFactory.swift new file mode 100644 index 0000000000..dd5eb7322e --- /dev/null +++ b/DuckDuckGo/AutofillHeaderViewFactory.swift @@ -0,0 +1,92 @@ +// +// AutofillHeaderViewFactory.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 UIKit +import SwiftUI +import Core + +protocol AutofillHeaderViewDelegate: AnyObject { + func handlePrimaryAction(for headerType: AutofillHeaderViewFactory.ViewType) + func handleDismissAction(for headerType: AutofillHeaderViewFactory.ViewType) +} + +protocol AutofillHeaderViewFactoryProtocol: AnyObject { + var delegate: AutofillHeaderViewDelegate? { get set } + + func makeHeaderView(for type: AutofillHeaderViewFactory.ViewType) -> UIViewController +} + +final class AutofillHeaderViewFactory: AutofillHeaderViewFactoryProtocol { + + weak var delegate: AutofillHeaderViewDelegate? + + enum ViewType { + case syncPromo(SyncPromoManager.Touchpoint) + case survey(AutofillSurveyManager.AutofillSurvey) + } + + init(delegate: AutofillHeaderViewDelegate?) { + self.delegate = delegate + } + + func makeHeaderView(for type: ViewType) -> UIViewController { + switch type { + case .syncPromo(let touchpointType): + return makeSyncPromoView(touchpointType: touchpointType) + case .survey(let survey): + return makeSurveyView(survey: survey) + } + } + + private func makeSyncPromoView(touchpointType: SyncPromoManager.Touchpoint) -> UIHostingController { + let headerView = SyncPromoView(viewModel: SyncPromoViewModel( + touchpointType: touchpointType, + primaryButtonAction: { [weak delegate] in + delegate?.handlePrimaryAction(for: .syncPromo(touchpointType)) + }, + dismissButtonAction: { [weak delegate] in + delegate?.handleDismissAction(for: .syncPromo(touchpointType)) + } + )) + + Pixel.fire(.syncPromoDisplayed, withAdditionalParameters: ["source": touchpointType.rawValue]) + + let hostingController = UIHostingController(rootView: headerView) + hostingController.view.backgroundColor = .clear + return hostingController + } + + private func makeSurveyView(survey: AutofillSurveyManager.AutofillSurvey) -> UIHostingController { + let headerView = AutofillSurveyView( + primaryButtonAction: { [weak delegate] in + delegate?.handlePrimaryAction(for: .survey(survey)) + }, + dismissButtonAction: { [weak delegate] in + delegate?.handleDismissAction(for: .survey(survey)) + } + ) + + Pixel.fire(pixel: .autofillManagementScreenVisitSurveyAvailable) + + let hostingController = UIHostingController(rootView: headerView) + hostingController.view.backgroundColor = .clear + return hostingController + } +} diff --git a/DuckDuckGo/AutofillLoginListViewModel.swift b/DuckDuckGo/AutofillLoginListViewModel.swift index 77ac3f4bb1..0bf99e6fe1 100644 --- a/DuckDuckGo/AutofillLoginListViewModel.swift +++ b/DuckDuckGo/AutofillLoginListViewModel.swift @@ -95,6 +95,7 @@ final class AutofillLoginListViewModel: ObservableObject { private let autofillDomainNameUrlMatcher = AutofillDomainNameUrlMatcher() private let autofillDomainNameUrlSort = AutofillDomainNameUrlSort() private let syncService: DDGSyncing + private let locale: Locale private var showBreakageReporter: Bool = false private lazy var reporterDateFormatter = { @@ -110,6 +111,8 @@ final class AutofillLoginListViewModel: ObservableObject { private lazy var syncPromoManager: SyncPromoManaging = SyncPromoManager(syncService: syncService) + private lazy var autofillSurveyManager: AutofillSurveyManaging = AutofillSurveyManager() + internal lazy var breakageReporter = BrokenSiteReporter(pixelHandler: { [weak self] _ in if let currentTabUid = self?.currentTabUid { NotificationCenter.default.post(name: .autofillFailureReport, object: self, userInfo: [UserInfoKeys.tabUid: currentTabUid]) @@ -118,7 +121,7 @@ final class AutofillLoginListViewModel: ObservableObject { self?.showBreakageReporter = false }, keyValueStoring: keyValueStore, storageConfiguration: .autofillConfig) - @Published private (set) var viewState: AutofillLoginListViewModel.ViewState = .authLocked + @Published private(set) var viewState: AutofillLoginListViewModel.ViewState = .authLocked @Published private(set) var sections = [AutofillLoginListSectionType]() { didSet { updateViewState() @@ -156,7 +159,8 @@ final class AutofillLoginListViewModel: ObservableObject { autofillNeverPromptWebsitesManager: AutofillNeverPromptWebsitesManager = AppDependencyProvider.shared.autofillNeverPromptWebsitesManager, privacyConfig: PrivacyConfiguration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig, keyValueStore: KeyValueStoringDictionaryRepresentable = UserDefaults.standard, - syncService: DDGSyncing) { + syncService: DDGSyncing, + locale: Locale = Locale.current) { self.appSettings = appSettings self.tld = tld self.secureVault = secureVault @@ -166,6 +170,7 @@ final class AutofillLoginListViewModel: ObservableObject { self.privacyConfig = privacyConfig self.keyValueStore = keyValueStore self.syncService = syncService + self.locale = locale if let count = getAccountsCount() { authenticationNotRequired = count == 0 || AppDependencyProvider.shared.autofillLoginSession.isSessionValid @@ -224,7 +229,7 @@ final class AutofillLoginListViewModel: ObservableObject { authenticator.logOut() } - func authenticate(completion: @escaping(AutofillLoginListAuthenticator.AuthError?) -> Void) { + func authenticate(completion: @escaping (AutofillLoginListAuthenticator.AuthError?) -> Void) { guard !isAuthenticating else { return } @@ -329,6 +334,24 @@ final class AutofillLoginListViewModel: ObservableObject { syncPromoManager.dismissPromoFor(.passwords) } + func getSurveyToPresent() -> AutofillSurveyManager.AutofillSurvey? { + guard locale.isEnglishLanguage, + viewState == .showItems || viewState == .empty, + !isEditing, + privacyConfig.isEnabled(featureKey: .autofillSurveys) else { + return nil + } + return autofillSurveyManager.surveyToPresent(settings: privacyConfig.settings(for: .autofillSurveys)) + } + + func surveyUrl(survey: String) -> URL? { + return autofillSurveyManager.buildSurveyUrl(survey, accountsCount: accountsCount) + } + + func dismissSurvey(id: String) { + autofillSurveyManager.markSurveyAsCompleted(id: id) + } + // MARK: Private Methods private func saveReport(for currentTabUrl: URL) { diff --git a/DuckDuckGo/AutofillLoginSettingsListViewController.swift b/DuckDuckGo/AutofillLoginSettingsListViewController.swift index b3823fd806..65a3c5c59a 100644 --- a/DuckDuckGo/AutofillLoginSettingsListViewController.swift +++ b/DuckDuckGo/AutofillLoginSettingsListViewController.swift @@ -149,21 +149,11 @@ final class AutofillLoginSettingsListViewController: UIViewController { return tableView }() - private lazy var syncPromoViewHostingController: UIHostingController = { - let headerView = SyncPromoView(viewModel: SyncPromoViewModel(touchpointType: .passwords, primaryButtonAction: { [weak self] in - self?.segueToSync(source: "promotion_passwords") - Pixel.fire(.syncPromoConfirmed, withAdditionalParameters: ["source": SyncPromoManager.Touchpoint.passwords.rawValue]) - }, dismissButtonAction: { [weak self] in - self?.viewModel.dismissSyncPromo() - self?.updateTableHeaderView() - })) - - Pixel.fire(.syncPromoDisplayed, withAdditionalParameters: ["source": SyncPromoManager.Touchpoint.passwords.rawValue]) - - let hostingController = UIHostingController(rootView: headerView) - hostingController.view.backgroundColor = .clear - return hostingController - }() + private lazy var headerViewFactory: AutofillHeaderViewFactoryProtocol = AutofillHeaderViewFactory(delegate: self) + private var currentHeaderHostingController: UIViewController? + + // This is used to prevent the Sync Promo from being displayed immediately after the Survey is dismissed + private var surveyPromptPresented: Bool = false private lazy var lockedViewBottomConstraint: NSLayoutConstraint = { NSLayoutConstraint(item: tableView, @@ -672,22 +662,67 @@ final class AutofillLoginSettingsListViewController: UIViewController { } private func updateTableHeaderView() { - if viewModel.shouldShowSyncPromo() { - guard tableView.frame != .zero, tableView.tableHeaderView != syncPromoViewHostingController.view else { - return + guard tableView.frame != .zero else { + return + } + + if let survey = viewModel.getSurveyToPresent() { + if shouldUpdateHeaderView(for: .survey(survey)) { + configureTableHeaderView(for: .survey(survey)) + surveyPromptPresented = true + } + return + } + + if viewModel.shouldShowSyncPromo() && !surveyPromptPresented { + if shouldUpdateHeaderView(for: .syncPromo(.passwords)) { + configureTableHeaderView(for: .syncPromo(.passwords)) } + return + } - addChild(syncPromoViewHostingController) + // No header view is needed, clear the table header + clearTableHeaderView() + } - let syncPromoViewHeight = syncPromoViewHostingController.view.sizeThatFits(CGSize(width: tableView.bounds.width - tableView.layoutMargins.left - tableView.layoutMargins.right, height: CGFloat.greatestFiniteMagnitude)).height - syncPromoViewHostingController.view.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: syncPromoViewHeight) - tableView.tableHeaderView = syncPromoViewHostingController.view + private func shouldUpdateHeaderView(for type: AutofillHeaderViewFactory.ViewType) -> Bool { + if let currentHeaderView = tableView.tableHeaderView, + let headerView = currentHeaderHostingController?.view, + currentHeaderView == headerView { + return false + } + return true + } - syncPromoViewHostingController.didMove(toParent: self) - } else { - guard tableView.tableHeaderView != nil else { - return + private func configureTableHeaderView(for type: AutofillHeaderViewFactory.ViewType) { + switch type { + case .survey(let survey): + currentHeaderHostingController = headerViewFactory.makeHeaderView(for: .survey(survey)) + if let hostingController = currentHeaderHostingController as? UIHostingController { + setupTableHeaderView(with: hostingController) + } + case .syncPromo(let promoType): + currentHeaderHostingController = headerViewFactory.makeHeaderView(for: .syncPromo(promoType)) + if let hostingController = currentHeaderHostingController as? UIHostingController { + setupTableHeaderView(with: hostingController) } + } + } + + private func setupTableHeaderView(with hostingController: UIViewController) { + addChild(hostingController) + + let viewWidth = tableView.bounds.width - tableView.layoutMargins.left - tableView.layoutMargins.right + let viewHeight = hostingController.view.sizeThatFits(CGSize(width: viewWidth, height: CGFloat.greatestFiniteMagnitude)).height + + hostingController.view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight) + tableView.tableHeaderView = hostingController.view + + hostingController.didMove(toParent: self) + } + + private func clearTableHeaderView() { + if tableView.tableHeaderView != nil { tableView.tableHeaderView = nil } } @@ -1115,6 +1150,38 @@ extension AutofillLoginSettingsListViewController { } } +// MARK: AutofillHeaderViewDelegate + +extension AutofillLoginSettingsListViewController: AutofillHeaderViewDelegate { + + func handlePrimaryAction(for headerType: AutofillHeaderViewFactory.ViewType) { + switch headerType { + case .survey(let survey): + if let surveyURL = viewModel.surveyUrl(survey: survey.url) { + LaunchTabNotification.postLaunchTabNotification(urlString: surveyURL.absoluteString) + self.dismiss(animated: true) + } + viewModel.dismissSurvey(id: survey.id) + case .syncPromo(let touchpoint): + segueToSync(source: "promotion_passwords") + Pixel.fire(.syncPromoConfirmed, withAdditionalParameters: ["source": touchpoint.rawValue]) + } + } + + func handleDismissAction(for headerType: AutofillHeaderViewFactory.ViewType) { + defer { + updateTableHeaderView() + } + + switch headerType { + case .survey(let survey): + viewModel.dismissSurvey(id: survey.id) + case .syncPromo: + viewModel.dismissSyncPromo() + } + } +} + extension NSNotification.Name { static let autofillFailureReport: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.notification.autofillFailureReport") } diff --git a/DuckDuckGo/AutofillSurveyManager.swift b/DuckDuckGo/AutofillSurveyManager.swift new file mode 100644 index 0000000000..c8b2d2581b --- /dev/null +++ b/DuckDuckGo/AutofillSurveyManager.swift @@ -0,0 +1,126 @@ +// +// AutofillSurveyManager.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 BrowserServicesKit +import Core +import RemoteMessaging + +protocol AutofillSurveyManaging { + func surveyToPresent(settings: PrivacyConfigurationData.PrivacyFeature.FeatureSettings) -> AutofillSurveyManager.AutofillSurvey? + func markSurveyAsCompleted(id: String) + func resetSurveys() + func buildSurveyUrl(_ url: String, accountsCount: Int) -> URL? +} + +final class AutofillSurveyManager: AutofillSurveyManaging { + + struct AutofillSurvey { + let id: String + let url: String + } + + @UserDefaultsWrapper(key: .autofillSurveysCompleted, defaultValue: []) + private var autofillSurveysCompleted: [String] + + private enum BucketName: String { + case none + case few + case some + case many + case lots + } + + private enum Constants { + static let surveysSettingsKey = "surveys" + static let surveysIdSettingsKey = "id" + static let surveysUrlSettingsKey = "url" + static let savedPasswordsQueryParam = "saved_passwords" + static let listQueryParam = "list" + } + + func surveyToPresent(settings: PrivacyConfigurationData.PrivacyFeature.FeatureSettings) -> AutofillSurvey? { + guard let surveys = settings[Constants.surveysSettingsKey] as? [[String: Any]] else { + return nil + } + + for survey in surveys { + guard let id = survey[Constants.surveysIdSettingsKey] as? String, + let url = survey[Constants.surveysUrlSettingsKey] as? String, + !hasCompletedSurvey(id: id) else { + continue + } + return AutofillSurvey(id: id, url: url) + } + + return nil + } + + func markSurveyAsCompleted(id: String) { + autofillSurveysCompleted.append(id) + } + + func resetSurveys() { + autofillSurveysCompleted.removeAll() + } + + func buildSurveyUrl(_ url: String, accountsCount: Int) -> URL? { + guard let surveyURL = URL(string: url) else { + return nil + } + + let surveyURLBuilder = DefaultRemoteMessagingSurveyURLBuilder(statisticsStore: StatisticsUserDefaults(), + vpnActivationDateStore: DefaultVPNActivationDateStore(), + subscription: nil) + let url = surveyURLBuilder.add(parameters: [.appVersion, .atb, .atbVariant, .daysInstalled, .hardwareModel, .osVersion, .vpnFirstUsed, .vpnLastUsed], to: surveyURL) + return addPasswordsCountSurveyParameter(to: url, accountsCount: accountsCount) + } + + private func hasCompletedSurvey(id: String) -> Bool { + autofillSurveysCompleted.contains(id) + } + + private func addPasswordsCountSurveyParameter(to surveyURL: URL, accountsCount: Int) -> URL { + guard var components = URLComponents(string: surveyURL.absoluteString) else { + assertionFailure("Could not build URL components from survey URL") + return surveyURL + } + + var queryItems = components.queryItems ?? [] + queryItems.append(URLQueryItem(name: Constants.savedPasswordsQueryParam, + value: bucketNameFrom(count: accountsCount))) + components.queryItems = queryItems + + return components.url ?? surveyURL + } + + private func bucketNameFrom(count: Int) -> String { + if count == 0 { + return BucketName.none.rawValue + } else if count < 4 { + return BucketName.few.rawValue + } else if count < 11 { + return BucketName.some.rawValue + } else if count < 50 { + return BucketName.many.rawValue + } else { + return BucketName.lots.rawValue + } + } +} diff --git a/DuckDuckGo/AutofillSurveyView.swift b/DuckDuckGo/AutofillSurveyView.swift new file mode 100644 index 0000000000..d61bc661e1 --- /dev/null +++ b/DuckDuckGo/AutofillSurveyView.swift @@ -0,0 +1,100 @@ +// +// AutofillSurveyView.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 Core +import DesignResourcesKit +import DuckUI +import SwiftUI + +struct AutofillSurveyView: View { + var primaryButtonAction: (() -> Void)? + var dismissButtonAction: (() -> Void)? + + var body: some View { + ZStack(alignment: .topTrailing) { + VStack(spacing: 8) { + Group { + Image(.passwordsDDG96X96) + .resizable() + .frame(width: 50, height: 50, alignment: .center) + + Text(verbatim: "Help us improve!") + .daxHeadline() + .foregroundColor(Color(designSystemColor: .textPrimary)) + .padding(.top, 8) + .frame(maxWidth: .infinity) + + Text(verbatim: "We want to make using passwords in DuckDuckGo better.") + .daxBodyRegular() + .foregroundColor(Color(designSystemColor: .textPrimary)) + .padding(.top, 4) + + Button { + primaryButtonAction?() + } label: { + HStack { + Text(verbatim: "Take Survey") + .daxButton() + } + } + .buttonStyle(PrimaryButtonStyle(compact: true, fullWidth: false)) + .padding(.top, 8) + } + .padding(.horizontal, 24) + } + .multilineTextAlignment(.center) + .padding(.vertical) + .padding(.horizontal, 8) + + VStack { + HStack { + Spacer() + Button { + dismissButtonAction?() + } label: { + Image(.close24) + .foregroundColor(.primary) + } + .frame(width: 44, height: 44) + .contentShape(Rectangle()) + .padding(0) + } + } + .alignmentGuide(.top) { dimension in + dimension[.top] + } + } + .background(RoundedRectangle(cornerRadius: 8.0) + .foregroundColor(Color(designSystemColor: .surface)) + ) + .padding([.horizontal, .top], 20) + .padding(.bottom, 30) + } + +} + +#Preview("Light") { + AutofillSurveyView() + .preferredColorScheme(.light) +} + +#Preview("Dark") { + AutofillSurveyView() + .preferredColorScheme(.dark) +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/Contents.json new file mode 100644 index 0000000000..c753560d35 --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "address-bar-bottom.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/address-bar-bottom.pdf b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/address-bar-bottom.pdf new file mode 100644 index 0000000000..bebdafcbae Binary files /dev/null and b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-bottom.imageset/address-bar-bottom.pdf differ diff --git a/DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/Contents.json new file mode 100644 index 0000000000..d99cb4b5de --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "address-bar-top.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/address-bar-top.pdf b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/address-bar-top.pdf new file mode 100644 index 0000000000..cfcc9f793a Binary files /dev/null and b/DuckDuckGo/DaxOnboarding.xcassets/address-bar-top.imageset/address-bar-top.pdf differ diff --git a/DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Contents.json b/DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Contents.json new file mode 100644 index 0000000000..bd1c3bd86b --- /dev/null +++ b/DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Shape.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Shape.pdf b/DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Shape.pdf new file mode 100644 index 0000000000..de47036a6a Binary files /dev/null and b/DuckDuckGo/DaxOnboarding.xcassets/checkShape.imageset/Shape.pdf differ diff --git a/DuckDuckGo/Debug.storyboard b/DuckDuckGo/Debug.storyboard index c38326876d..313b40911f 100644 --- a/DuckDuckGo/Debug.storyboard +++ b/DuckDuckGo/Debug.storyboard @@ -55,6 +55,9 @@ + + + @@ -357,7 +360,7 @@ - + @@ -491,12 +494,21 @@ + + + + + + + + + - + @@ -505,7 +517,7 @@ - + @@ -962,17 +974,17 @@ - + - + - + - + diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift b/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift index 7206c2b018..19decaf548 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift @@ -91,14 +91,7 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { @UserDefaultsWrapper(key: .duckPlayerPixelExperimentCohort, defaultValue: nil) var experimentCohort: String? - @UserDefaultsWrapper(key: .duckPlayerPixelExperimentOverride, defaultValue: false) - var experimentOverride: Bool { - didSet { - if experimentOverride { - enrollmentDate = Date() - } - } - } + private var isInternalUser: Bool enum Cohort: String { case control @@ -109,11 +102,13 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { referrer: DuckPlayerReferrer? = nil, userDefaults: UserDefaults = UserDefaults.standard, pixel: DuckPlayerExperimentPixelFiring.Type = Pixel.self, - dateProvider: DuckPlayerExperimentDateProvider = DefaultDuckPlayerExperimentDateProvider()) { + dateProvider: DuckPlayerExperimentDateProvider = DefaultDuckPlayerExperimentDateProvider(), + isInternalUser: Bool = false) { self.referrer = referrer self.duckPlayerMode = duckPlayerMode self.pixel = pixel self.dateProvider = dateProvider + self.isInternalUser = isInternalUser } private var dates: (day: Int, week: Int)? { @@ -144,12 +139,16 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { } var isExperimentCohort: Bool { - return experimentCohort == "experiment" || experimentOverride + return experimentCohort == "experiment" } func assignUserToCohort() { if !isEnrolled { - let cohort: Cohort = Bool.random() ? .experiment : .control + var cohort: Cohort = Bool.random() ? .experiment : .control + + if isInternalUser { + cohort = .experiment + } experimentCohort = cohort.rawValue enrollmentDate = dateProvider.currentDate fireEnrollmentPixel() @@ -226,12 +225,14 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { lastDayPixelFired = nil lastWeekPixelFired = nil lastVideoIDReported = nil - experimentOverride = false } func override() { enrollmentDate = Date() experimentCohort = "experiment" + lastDayPixelFired = nil + lastWeekPixelFired = nil + lastVideoIDReported = nil } diff --git a/DuckDuckGo/DuckPlayer/Resources/DuckPlayer-ModalAnimation.json b/DuckDuckGo/DuckPlayer/Resources/DuckPlayer-ModalAnimation.json index a4ab771677..a334e04618 100644 --- a/DuckDuckGo/DuckPlayer/Resources/DuckPlayer-ModalAnimation.json +++ b/DuckDuckGo/DuckPlayer/Resources/DuckPlayer-ModalAnimation.json @@ -1 +1 @@ -{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":7.00000028511585,"op":368.000014988947,"w":320,"h":180,"nm":"Comp 1","ddd":0,"assets":[{"id":"image_0","w":20,"h":20,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAABRklEQVQ4jbXVMWuDQBQH8P/TcwilBFKSpWRJHZ0zmTF7+jW6Cv0QBdd8jWbvGKfMjsYldEmoICVk8MzrUC1yJ9Yq/Y/Pdz/Ow3cSlDDzEsAjgJn6TEkM4JWI3qpFqkAPALwWUB3sE9H+ByywFwA3f8TKnAE8E9HeKApeDwzFWg8ARHFm2msmKQ+CMLcPp+skkxAAYAnI6dg4uo4ZjYZ0UZbMmHlJzLxWwfcPvt1ss3kJqbEE5Gph7e7v6FN5FBsqlqQ8aMIAIJMQm202T1IeqLs01OYgzO0mrIoGYW6rdQ08nK6T37CmXg1ss7umXg3sGw20BGTbxXW9GjgdG8e2YF2vBrqOGbXZpSUgXceM6sC4WhgN6bJaWLsmtPywa6YlpmL0PHVRh9EDAL+8bbTx65CYiJ7KM/TxfQV1zbkw/umCrabvL+AL4d2TwJBKfRgAAAAASUVORK5CYII=","e":1},{"id":"image_1","w":42,"h":9,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAJCAYAAABE+77DAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAbUlEQVQ4je3ToQ3CUBRA0fO+ZYDimYIBmIiEWViCBN8tOgC6A/Trh8I1FEHyEJwJrrn8fVdAZg4YiltWRcQEkZknnIt73nngEpl5w666ZsPY/H4k7Bt6dcUH5oZrdcWGjvvr+gOOtT2rOsaIWJ6fBxfBNPVVKAAAAABJRU5ErkJggg==","e":1},{"id":"image_2","w":67,"h":17,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEMAAAARCAYAAACGjBGPAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAACkUlEQVRYheWXz0tUURTHP+f1hplnjtaUkyGpyVhCNBCBoeCyRatsU7uCVlKLFpK08R+QoEUgErQtWtWuRYtsYxHhYANJ+COkFJxq0BydkRnnuph5M883o74RfaJ9YODec+65833nvXvuvYINpVQYuAGEgaN2/yEgBowAr0Vk3uoQs6GUqgZ6gQ53te0by8CQiLwzDQKFRAwALSUhC8OBQtvXmMTXktxzme7yRkSGAPS8oRd7ImYHm4i9DJFN6hvstV1zNPePowcybih1gW6l1FcRGZF8jRjY4P7R30b8bdOm4d4zS7Q9/3yIEhITkds6uWJZZGE4sGUiAFZ/+pl6dJHzzyI2jw/w7qrMMv8OpKyG5KrSIxNrDatp8WwX7PWo9KXWI7OGV6wvMqiU6tTJ7RpF5l9snQiTxGiQ1LRhqSG1+Z8b/AaSkEvEq/eZ9oWE8jsNjkxmQ3eveT7YEtKhYd8+V8YDOCURtQpwLGYXOG42IhNrDZUkAiCdQf/0LdtsM9drJSPtBXMrVr7XWHqlc+0dBY1OloZTSh/AE3C+dda0xy09N4tpQWPwmPzbyQQ11VLynBq5E1kR/5WYfVBZNCNDddgq5A+Q3YmwCskAi2bnwlkt1tYoM5VM0HJam7vcqs3azGOilOoBugum1LTB+J3ObZdL3c0pGvsmy3h8lQjbAalyxviiMv4mssZ2wVU+Ld1wQpbKuO6LUuoUMIi1kCZG/Uw8aN80IbVdc4SeRB1JPxhEReSheRy/Su4UWiQ1bfDraYilL8FCUqrOxam7NcPJ686W0sFgGbgnIvPWi9rG5fJ/sAz0icgUWG6tAEqpTqAHCO6DMLeJAo+t13gpNyq/bMJAvUvC3GQM+Gh+DVbWAVNnztan4+NpAAAAAElFTkSuQmCC","e":1},{"id":"image_3","w":141,"h":49,"u":"","p":"data:image/png;base64,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","e":1},{"id":"image_4","w":29,"h":4,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAECAYAAABySjRcAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAMElEQVQYlb3NQREAIAwEsYABrFQKUpBeB4eL7sy+I1TokKlXaByDbbxJEBaEi5pCP6MOPNpI69kPAAAAAElFTkSuQmCC","e":1},{"id":"image_5","w":101,"h":4,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGUAAAAECAYAAACX1PEwAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAQUlEQVQ4jWO8e/fuf4ZRMCgAIyPjQiUlpQTG0UgZdGAi00C7YBSggI9MTEwLWBgYGBoH2iWjAAKYmJg2KCoqXgAApAUNMclgLKQAAAAASUVORK5CYII=","e":1},{"id":"image_6","w":141,"h":87,"u":"","p":"data:image/png;base64,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","e":1},{"id":"image_7","w":141,"h":18,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI0AAAASCAYAAABmbl0zAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAABd0lEQVRoge3Zv2rCUBQG8C/RJYJUcAoKLQTrJqWpD+FQcO0TdMvkkzjqSxQKOlq3Ll1utuwODtdFMbjkdupiE//de5pIz29MyJcD+cjlJla3231IkuQNwC0YO0ApNU+SpF9yXfcTXBh2Asuy7mzbdss4UBh/vT47+Kta1ZmLFV+/nHVmFEV4vKA0kePgtd3GulTSmowV1o2ddvRZyosKAwD3cYyX5VJrKlZsqW8ad7fTCn3abDDaO9ZqtbQy90kpsVqtjGay02QuT6ZUKhUEQYBGo2E8ezqdYjKZGM9lh6UuTyb1ej2SwvxkN5tNkmyWjbw01A+10+mQ5rPfyEtDTUqZ9wj/DnlpZrMZWXYcxwjDkCyfpSMvjRAC4/EYi8XCaG4YhhgOh9hut0Zz2XEku6f9D3tCCAghKG7FcpD6ptH9FfBer2tdz4otszQDz0PkOGeFRY6Dgefho1YzMhwrJsv3fZX3EOy6XP2Wm/29slJqnvcQ7Lp8A7ghXrH1RxAYAAAAAElFTkSuQmCC","e":1},{"id":"image_8","w":405,"h":708,"u":"","p":"data:image/jpeg;base64,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","e":1}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"Channel.pdf","cl":"pdf","refId":"image_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":64,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":360.000014663101,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[220.75,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":64,"s":[262.25,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[262.25,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":360.000014663101,"s":[220.75,47.75,0]}],"ix":2},"a":{"a":0,"k":[10,10,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":2,"nm":"Text Ad.pdf","cl":"pdf","refId":"image_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":64,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":360.000014663101,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[120.75,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":64,"s":[59.25,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[59.25,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":360.000014663101,"s":[120.75,43.75,0]}],"ix":2},"a":{"a":0,"k":[21,4.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":2,"nm":"Ad.pdf","cl":"pdf","refId":"image_2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":64,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":360.000014663101,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[130.5,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":64,"s":[48,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[48,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"t":360.000014663101,"s":[130.5,95.875,0]}],"ix":2},"a":{"a":0,"k":[33.5,8.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":2,"nm":"Description.pdf","cl":"pdf","refId":"image_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":64,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":360.000014663101,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[161.417,143,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":64,"s":[162.917,204,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":349,"s":[162.917,204,0],"to":[0,0,0],"ti":[0,0,0]},{"t":360.000014663101,"s":[161.417,143,0]}],"ix":2},"a":{"a":0,"k":[70.5,24.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":2,"nm":"Line 9.pdf","cl":"pdf","parent":7,"refId":"image_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":64,"s":[20.208,79.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":76,"s":[20.208,80.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":92,"s":[20.208,80.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":341,"s":[20.208,80.125,0],"to":[0,0,0],"ti":[0,0,0]},{"t":355.000014459446,"s":[20.208,79.125,0]}],"ix":2},"a":{"a":0,"k":[14.5,2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ef":[{"ty":20,"nm":"Tint","np":6,"mn":"ADBE Tint","ix":1,"en":1,"ef":[{"ty":2,"nm":"Map Black To","mn":"ADBE Tint-0001","ix":1,"v":{"a":0,"k":[1,0.800000071526,0.200000017881,1],"ix":1}},{"ty":2,"nm":"Map White To","mn":"ADBE Tint-0002","ix":2,"v":{"a":0,"k":[0.93412989378,0.700597524643,0,1],"ix":2}},{"ty":0,"nm":"Amount to Tint","mn":"ADBE Tint-0003","ix":3,"v":{"a":1,"k":[{"i":{"x":[0.142],"y":[1]},"o":{"x":[0.951],"y":[0.71]},"t":47,"s":[100]},{"i":{"x":[0.142],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":341,"s":[0]},{"t":355.000014459446,"s":[100]}],"ix":3}},{"ty":6,"nm":"","mn":"ADBE Tint-0004","ix":4,"v":0}]}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":2,"nm":"Line 10.pdf","cl":"pdf","parent":7,"refId":"image_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":64,"s":[85.708,79.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":76,"s":[85.708,80.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":92,"s":[85.708,80.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":341,"s":[85.708,80.125,0],"to":[0,0,0],"ti":[0,0,0]},{"t":355.000014459446,"s":[85.708,79.125,0]}],"ix":2},"a":{"a":0,"k":[50.5,2,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":64,"s":[101.485,100,100]},{"t":92.0000037472368,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":2,"nm":"Duck PLayer.pdf","cl":"pdf","refId":"image_6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[162.917,75.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":64,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":92,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":341,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"t":355.000014459446,"s":[162.917,75.75,0]}],"ix":2},"a":{"a":0,"k":[70.5,43.5,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[0.978,0.978,1]},"o":{"x":[0.055,0.055,0.333],"y":[0.409,0.409,0]},"t":99,"s":[100,100,100]},{"i":{"x":[0.348,0.348,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":123,"s":[167.816,167.816,100]},{"i":{"x":[0.858,0.858,0.667],"y":[1.003,1.003,1]},"o":{"x":[0.527,0.527,0.167],"y":[0.386,0.386,0]},"t":341,"s":[167.816,167.816,100]},{"t":355.000014459446,"s":[100,100,100]}],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[0,0],[0,87],[141,87],[141,0]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":2,"nm":"Top Bar.pdf","cl":"pdf","refId":"image_7","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":64,"s":[3]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":346,"s":[3]},{"t":360.000014663101,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[163.042,23.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":64,"s":[162.917,-14.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":346,"s":[162.917,-14.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":360.000014663101,"s":[163.042,23.75,0]}],"ix":2},"a":{"a":0,"k":[70.5,9,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":1,"nm":"Black Solid 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1.016]},"o":{"x":[0.077],"y":[0.68]},"t":99,"s":[9]},{"i":{"x":[0],"y":[8.191]},"o":{"x":[0.167],"y":[0]},"t":123,"s":[59]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":341,"s":[59]},{"t":355.000014459446,"s":[9]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160.5,88,0],"ix":2},"a":{"a":0,"k":[160,90,0],"ix":1},"s":{"a":0,"k":[105,108.889,100],"ix":6}},"ao":0,"sw":320,"sh":180,"sc":"#000000","ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":2,"nm":"Rectangle.png","cl":"png","refId":"image_8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1.038]},"o":{"x":[0.027],"y":[1.042]},"t":99,"s":[0]},{"i":{"x":[0],"y":[35.719]},"o":{"x":[0.167],"y":[0]},"t":123,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":341,"s":[100]},{"t":355.000014459446,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[132.5,-168.5,0],"ix":2},"a":{"a":0,"k":[202.5,354,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":55.0000022401959,"op":955.000038897947,"st":55.0000022401959,"bm":0}],"markers":[]} \ No newline at end of file +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":368.000014988947,"w":320,"h":180,"nm":"Comp 1","ddd":0,"assets":[{"id":"image_0","w":20,"h":20,"u":"","p":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSgBBwcHCggKEwoKEygaFhooKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKP/AABEIABQAFAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/APprWtVg0i08+4ySTtRF6sa3w+HlXlyxIqVFBXZh6X4zt7u7SC4t2tw52q+/cM+/AxXZWyydOPNF3sYwxKk7NWOrrzDpOU+IGnz3dnbz26NIIC29VGTg45x7Y/WvTyytGnNxlpc5sTBySa6HE6Vp1xqN4kFujElhubHCD1Jr2a1aFGDlJnJCDm7I9hr5M9QKACgAoA//2Q==","e":1},{"id":"image_1","w":42,"h":6,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAGCAYAAAC1rQwWAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAKElEQVQokWP8////AYbBDy6wMDAw2A+0K4gBTAPtAGLBqEOpDYaMQwFnXQTaXe9dvQAAAABJRU5ErkJggg==","e":1},{"id":"image_2","w":67,"h":17,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEMAAAARCAYAAACGjBGPAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAABsUlEQVRYhe2WPS9EQRSGnxl31374WB+7IkE0ZEVIhEoiOoVKI9lo/AR+gl+gkvgNCoVGJWELjQaJRISCsGJ9rYRgl3tHoSFzd+9dca8NnvLMOZN33jlnMkIptcE/ADsGMPrTKioF+dMCKgmj6Ir5BFdLkFuDl2sQQagZgEQKoj0+SvQPoZRSWvT5BI5mIH9mUyKhbRZaprxX5y9pvTPMpxJGAFhwNg+BZmgc81ZeGby8KnIPCpur/URDjSAYELZruhk3KyWM+EBmoWLM2D402dwzeTWdc6WAoaRkuFc/uv6A5tbdKSicw+OBu1wPubyzSO+6MwLAUrC1b3F8YWlruhn5U/dKCln3uR6RvXWYiyJc2NTpZhgx9zvK0JeEfCfRkP38O9fpMd2M2kF3u4kgRPu+JOQ76WwVtMfLMyQREyQ7bPpAz0zB1TKoQukd45NQFS5LhBdIIZgYMTjKWOTunfPrItDVJgkYuoH2/4ybVTieA/RHBoBoP3QvgqwuT3llk7b/jjeNvx82kvwclxFomf6NRgDFOuMj+cz7j9Soh3A3yIBP0nwn7WzG36HImPxR3gAgSnf+tTIOIgAAAABJRU5ErkJggg==","e":1},{"id":"image_3","w":141,"h":49,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI0AAAAxCAYAAADnViqrAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAE6ElEQVR4nO2cTW8jNRjH/36ZSSZJ36sWVKkFLQtCohIcVkhw4MQNiQNfAS58BC6c+AYc+ATcEReEhFhx4bSIwyLx0iKBtIt2K9Ldtklmxh6bwzJJJnGaTJrMOIl/t9jJ+Innbz+PH4+HnJyc3IXDkQNOCHmnbCMciwUt2wDH4sFNhYQQcM7BGAMhpGibHCWjlEKSJJBSGuuHRMMYg+/7TiwrDGMMjDF4nocoiqCUytRnREMpHSkYologOjI2okgNoNUZmu2wAUIIKpUKOp1OpjwjGs/zsoLRErWrr1FrfwMuH4y8uAaB8F5Ga+0DRMFbs7XcUSqEEHieByFEr+z09FSnlUEQ9L6tQmz/+yn8+JdcjbTq7+Ny86Oh8nTKs83taa0hpRyagh09tNaZ2aY70wzezPWnX+QWDADUW19Bei+gU3+3W+Z5HjzPm8beQmCMGX23DYQxwXmLQ+n5tuMxjZ01CWZYTw9qwygaKh8jaH83tQGNyy8zouHcuEizhnS1GMdx2aZ0aYUU9/6s4WHTA1DM7Myoxu3nQxwfdYziSTFWVaKfQDC9tFlyBi7+mvr3q85lh+Lbn9fxsOmjKMEAQKIIfn0Q4O79NSTXTLpG0bCkeWMDmHzUMyZJbny9eWOTjT/+1kAky8u7nl14uP93MLK+EL+RTvuMsSKay0UaCNsimsdPOZpX5bvzP/6p4viwA2rQbmHW2RQv2MzZRfmCAQCZEJy3GHbWhgeT23uyDK3tSUnEI1xkobKmprlugdFaQ+vZroWrvj3L/rXA7LILEU2aVbQtsTcLZh0PHWzHuEdqpc84Ww2JRtUsYOPQ1+TmiThNe9H3sgoGmH1wH/garx6EM71mfjTeeLE9stYoGsmPbtgkgeCH3c/LKhjg2X+btds9Purg1nPlCIcSjTdvt7C3YX4sAhjhnqLq60joNpiaLl8TVe9As43uZ6XU0sUzKVrrmW8/EALceamNw90Yp48qaF5ySDXfgVfhCnsbEq8chCPdUkpXNJmAjvi42PwYm83PcmeGFanhcuPDTJmUcildVJrjmRf7mxL7m/O7/qQMBvvdXW4ACIIgc2Or7R+w/uRzUD3av/Uj2T6ebH8C6d8y1hNClkY481g52UqSJIii3rNUGdGYdqOJaiFofw8v/h1EXxkvqtgOYv81hMHbALEjOeWYHVEUZVaImTsshABjLBN/aFpHu/FecRY6rMKUUhiKTsMwnKufdiwGWmsIIYzbP0ZfEscxhBCglC5NDOKYHK31tQnLkQHIuB86VpflTJ445ooTjSM3TjSO3DjROHLjROPIzcTpW0rpUOJvlRh3qC49BlNk/6SbpUXn1SYSDed8KTcc88I5hxAic0QVeDagKpVKaf3DOUcURYXthY0dFpRSJ5g+PM8bmk3KFAzQe3FDYe2N+wLn3AlmgP4To7acTy/SjrGisaFDbKN/prEpxivKlrGtrMozI3noD4ZtemlAUbaMFY2U0glngP7VSpIkVvRPkXaMFY1SCkIIKzrGBoQQQyO6yJWLCaVUoSdYJ1pyp/mJVQ6Kr8vTKKUQhqHL0wxStJoXjfShpVXAntDfsTA40ThyQ23KMzgWA5p5o6fDMQF0a2trZVdEjumgvu9jb2/PCccxEY1GA0T/n5WK4xjn5+fodDpWpcYddkAIQb1ex+7uLv4DTsP3dx/5jJUAAAAASUVORK5CYII=","e":1},{"id":"image_4","w":141,"h":18,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI0AAAASCAYAAABmbl0zAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAABzUlEQVRoge3YsUsCURwH8K+VHppGmBWXKIIQt4Z/gBwt0n/R0ugf4F8gTq5Jg6JTszlGuAUORdtNguDkkUle3B3na2qonmH6Xmr8Ptvdgy+/4cu9987HGLurVquoVCqwbRuE8ESjURQKBei6Dl+9XmflcnnZM5E1EAgEUCwW4dN1nY1Go2XPQ9ZELBbD1rTCHFsWLvp9HLruzIEvm5u4UlU8RCKiZiQrZjAYYIu3cOA4uDQMRDzv16Enr6841zQYodDCA5LVtMF7efr8PFdhAEBhDGemudBQZLVxvzTzFuZDaDL59JxKpZDL5RARuG11u120Wi2Mx2NhmWQ23NKIlEgkkM/n4ff7heYmk0mk02mUSiVMvpSUyMXdnkTKZrPCC/MhHo9D0zQp2WQ66aUJBoNS88PhsNR88p300hiGIS3b8zyp+YRPemna7TY6nY7wXMdx0Gg0MBwOhWeTn0k/CDPGUKvV0Gw2oaqqkEzXddHr9WBZlpA88jvc0jwueE542t7+9s40TZj0/+Zf4G5P9zs7uN7fnyvwdncXN3t7Cw1FVpsvk8mwaYtHto3029vMYT1FQVfybYks349nmr6ioK8ofzULWRPSb0/k/3kHh4mHa1Tzd/gAAAAASUVORK5CYII=","e":1},{"id":"image_5","w":29,"h":4,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAECAYAAABySjRcAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAMElEQVQYlb3NQREAIAwEsYABrFQKUpBeB4eL7sy+I1TokKlXaByDbbxJEBaEi5pCP6MOPNpI69kPAAAAAElFTkSuQmCC","e":1},{"id":"image_6","w":101,"h":4,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGUAAAAECAYAAACX1PEwAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAAQUlEQVQ4jWO8e/fuf4ZRMCgAIyPjQiUlpQTG0UgZdGAi00C7YBSggI9MTEwLWBgYGBoH2iWjAAKYmJg2KCoqXgAApAUNMclgLKQAAAAASUVORK5CYII=","e":1},{"id":"image_7","w":141,"h":87,"u":"","p":"data:image/png;base64,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","e":1},{"id":"image_8","w":405,"h":708,"u":"","p":"data:image/jpeg;base64,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","e":1}],"layers":[{"ddd":0,"ind":1,"ty":1,"nm":"Pale Orange Solid 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.075],"y":[0.996]},"t":135,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":139,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[1],"y":[0.013]},"t":150,"s":[100]},{"t":151.000006150356,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160,90,0],"ix":2},"a":{"a":0,"k":[160,90,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.326,"y":1},"o":{"x":0.739,"y":0.739},"t":139,"s":[{"i":[[0,0],[2.625,-4.125],[0,0],[-8.289,-1.382],[-2.125,-2.625],[2.5,3.25]],"o":[[0,0],[-1.101,1.73],[0,0],[6.75,1.125],[0.957,1.182],[-2.5,-3.25]],"v":[[157.5,63],[146.125,67],[146.375,71.5],[158.375,65.625],[169.875,72.5],[169.5,68.25]],"c":true}]},{"i":{"x":0.302,"y":0.302},"o":{"x":0.752,"y":0},"t":144,"s":[{"i":[[0,0],[2.625,-4.125],[0,0],[-11.125,-1],[-0.75,4.375],[2.5,3.25]],"o":[[0,0],[-1.101,1.73],[0,0],[6.816,0.613],[0.275,-1.606],[-2.5,-3.25]],"v":[[157.5,63],[146.125,67],[145.125,76],[157.5,85.25],[171.125,76.5],[169.5,68.25]],"c":true}]},{"t":150.000006109625,"s":[{"i":[[0,0],[2.625,-4.125],[0,0],[-8.289,-1.382],[-2.125,-2.625],[2.5,3.25]],"o":[[0,0],[-1.101,1.73],[0,0],[6.75,1.125],[0.957,1.182],[-2.5,-3.25]],"v":[[157.5,63],[146.125,67],[146.375,71.5],[158.375,65.625],[169.875,72.5],[169.5,68.25]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ef":[{"ty":20,"nm":"Tint","np":6,"mn":"ADBE Tint","ix":1,"en":1,"ef":[{"ty":2,"nm":"Map Black To","mn":"ADBE Tint-0001","ix":1,"v":{"a":0,"k":[0.65986520052,0.609302341938,0.562351107597,1],"ix":1}},{"ty":2,"nm":"Map White To","mn":"ADBE Tint-0002","ix":2,"v":{"a":0,"k":[1,0.939583837986,0.883195459843,1],"ix":2}},{"ty":0,"nm":"Amount to Tint","mn":"ADBE Tint-0003","ix":3,"v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":139,"s":[0]},{"t":144.00000586524,"s":[100]}],"ix":3}},{"ty":6,"nm":"","mn":"ADBE Tint-0004","ix":4,"v":0}]}],"sw":320,"sh":180,"sc":"#d4beb0","ip":130.000005295009,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":161,"s":[100]},{"t":188.000007657397,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[134.5,59.25,0],"ix":2},"a":{"a":0,"k":[-14,-11,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,18.987]},"t":161,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":188,"s":[90,90,100]},{"t":297.000012097058,"s":[90,96.41,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":4,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":6,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":15.1,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.964307598039,0.948043763404,0.948043763404,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-14,-10.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":58.0000023623884,"op":958.00003902014,"st":58.0000023623884,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":144,"s":[100]},{"t":173.000007046434,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[136,76.5,0],"ix":2},"a":{"a":0,"k":[-14,-11,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,18.987]},"t":144,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":173,"s":[90,90,100]},{"t":297.000012097058,"s":[90,96.41,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":4,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":6,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":15.1,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.964307598039,0.948043763404,0.948043763404,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-14,-10.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":41.0000016699642,"op":941.000038327716,"st":41.0000016699642,"bm":0},{"ddd":0,"ind":4,"ty":2,"nm":"Channel.eps","cl":"eps","refId":"image_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[80]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":358.000014581639,"s":[80]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[218.75,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":59,"s":[262.25,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[262.25,47.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[218.75,47.75,0]}],"ix":2},"a":{"a":0,"k":[10,10,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[5.523,0],[0,-5.523],[-5.523,0],[0,5.523]],"o":[[-5.523,0],[0,5.523],[5.523,0],[0,-5.523]],"v":[[10,0],[0,10],[10,20],[20,10]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":2,"nm":"Text Ad.eps","cl":"eps","refId":"image_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[80]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":358.000014581639,"s":[80]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[118.75,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":59,"s":[59.25,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[59.25,43.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[118.75,43.75,0]}],"ix":2},"a":{"a":0,"k":[21,3,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[1.657,0],[0,0],[0,-1.657],[-1.657,0],[0,0],[0,1.657]],"o":[[0,0],[-1.657,0],[0,1.657],[0,0],[1.657,0],[0,-1.657]],"v":[[39,0],[3,0],[0,3],[3,6],[39,6],[42,3]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":2,"nm":"Ad.eps","cl":"eps","refId":"image_2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[80]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":358.000014581639,"s":[80]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[132,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":59,"s":[48,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":349,"s":[48,95.875,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[132,95.875,0]}],"ix":2},"a":{"a":0,"k":[33.5,8.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[4.694,0],[0,0],[0,-4.694],[-4.694,0],[0,0],[0,4.694]],"o":[[0,0],[-4.694,0],[0,4.694],[0,0],[4.694,0],[0,-4.694]],"v":[[58.5,0],[8.5,0],[0,8.5],[8.5,17],[58.5,17],[67,8.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":2,"nm":"Description","refId":"image_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[80]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[0]},{"t":358.000014581639,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":47,"s":[163.167,143,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":59,"s":[162.667,204,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":349,"s":[162.667,204,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[163.167,143,0]}],"ix":2},"a":{"a":0,"k":[70.5,24.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0.046,0],[0,0],[0,-0.035],[0,0],[-4.418,0],[0,0],[0,4.119],[0,0]],"o":[[0,0],[-0.035,0],[0,0],[0,4.418],[0,0],[4.119,0],[0,0],[0,-0.046]],"v":[[141.208,-0.5],[-0.104,-0.5],[-0.167,-0.438],[-0.167,41.216],[7.833,49.216],[133.833,49.216],[141.292,41.757],[141.292,-0.416]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":2,"nm":"Top Bar.eps","cl":"eps","refId":"image_4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":16,"s":[99]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[3]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":349,"s":[3]},{"t":358.000014581639,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":47,"s":[163.042,24.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":59,"s":[162.917,-14.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":349,"s":[162.917,-14.75,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[163.042,24.75,0]}],"ix":2},"a":{"a":0,"k":[70.5,9,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[2.401,0],[0,0],[0,-2.209],[0,-2.557],[0,0],[0,2.402]],"o":[[0,0],[-2.209,0],[0,2.209],[0,0],[0,-3.466],[0,-2.402]],"v":[[136.039,0.5],[4.396,0.5],[0.396,4.5],[0.41,17.432],[140.311,17.432],[140.387,4.848]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":2,"nm":"Line 9.pdf","cl":"pdf","parent":12,"refId":"image_5","sr":1,"ks":{"o":{"a":0,"k":99,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[6.107,80.125,0],"ix":2},"a":{"a":0,"k":[0.25,2,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":59,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":346,"s":[161,100,100]},{"t":360.000014663101,"s":[100,100,100]}],"ix":6}},"ao":0,"ef":[{"ty":20,"nm":"Tint","np":6,"mn":"ADBE Tint","ix":1,"en":1,"ef":[{"ty":2,"nm":"Map Black To","mn":"ADBE Tint-0001","ix":1,"v":{"a":0,"k":[1,0.800000071526,0.200000017881,1],"ix":1}},{"ty":2,"nm":"Map White To","mn":"ADBE Tint-0002","ix":2,"v":{"a":0,"k":[0.93412989378,0.700597524643,0,1],"ix":2}},{"ty":0,"nm":"Amount to Tint","mn":"ADBE Tint-0003","ix":3,"v":{"a":1,"k":[{"i":{"x":[0.142],"y":[1]},"o":{"x":[0.951],"y":[0.71]},"t":47,"s":[100]},{"i":{"x":[0.142],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":346,"s":[0]},{"t":360.000014663101,"s":[100]}],"ix":3}},{"ty":6,"nm":"","mn":"ADBE Tint-0004","ix":4,"v":0}]}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":2,"nm":"Line 10.pdf","cl":"pdf","parent":12,"refId":"image_6","sr":1,"ks":{"o":{"a":0,"k":99,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[135.838,80.125,0],"ix":2},"a":{"a":0,"k":[101.076,2,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":59,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":346,"s":[83,100,100]},{"t":360.000014663101,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":2,"nm":"Duck PLayer.eps","cl":"eps","refId":"image_7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[162.917,75.75,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":61,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":83,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":348,"s":[162.917,91.25,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[162.917,75.75,0]}],"ix":2},"a":{"a":0,"k":[70.5,43.5,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[0.987,0.987,1]},"o":{"x":[0.055,0.055,0.333],"y":[0.238,0.238,0]},"t":69,"s":[100,100,100]},{"i":{"x":[0.348,0.348,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":83,"s":[167.816,167.816,100]},{"i":{"x":[0.858,0.858,0.667],"y":[1.002,1.002,1]},"o":{"x":[0.527,0.527,0.167],"y":[0.276,0.276,0]},"t":348,"s":[167.816,167.816,100]},{"t":358.000014581639,"s":[100,100,100]}],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":47,"s":[{"i":[[-0.074,0],[0,0],[0,-0.029],[0,0],[0.006,0],[0,0],[0,-0.007],[0,0]],"o":[[0,0],[-0.055,0],[0,0],[0,-0.006],[0,0],[-0.007,0],[0,0],[0,0.074]],"v":[[140.892,0.447],[0.333,0.447],[0.235,0.5],[0.235,85.625],[0.223,85.614],[140.771,85.614],[140.758,85.627],[140.758,0.312]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[1.105,0],[0,0],[0,-1.105],[0,0],[-1.105,0],[0,0],[0,1.105],[0,0]],"o":[[0,0],[-1.105,0],[0,0],[0,1.105],[0,0],[1.105,0],[0,0],[0,-1.105]],"v":[[138.758,0.447],[2.235,0.447],[0.235,2.447],[0.235,83.614],[2.235,85.614],[138.758,85.614],[140.758,83.614],[140.758,2.447]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":348,"s":[{"i":[[1.105,0],[0,0],[0,-1.105],[0,0],[-1.105,0],[0,0],[0,1.105],[0,0]],"o":[[0,0],[-1.105,0],[0,0],[0,1.105],[0,0],[1.105,0],[0,0],[0,-1.105]],"v":[[138.758,0.447],[2.235,0.447],[0.235,2.447],[0.235,83.614],[2.235,85.614],[138.758,85.614],[140.758,83.614],[140.758,2.447]],"c":true}]},{"t":358.000014581639,"s":[{"i":[[-0.074,0],[0,0],[0,-0.029],[0,0],[0.006,0],[0,0],[0,-0.007],[0,0]],"o":[[0,0],[-0.055,0],[0,0],[0,-0.006],[0,0],[-0.007,0],[0,0],[0,0.074]],"v":[[140.892,0.447],[0.333,0.447],[0.235,0.5],[0.235,85.625],[0.223,85.614],[140.771,85.614],[140.758,85.627],[140.758,0.312]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":1,"nm":"Dark Gray Solid 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":0,"s":[7]},{"i":{"x":[0],"y":[1.014]},"o":{"x":[0.077],"y":[0.601]},"t":69,"s":[6]},{"i":{"x":[0],"y":[9.742]},"o":{"x":[0.167],"y":[0]},"t":83,"s":[39]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":348,"s":[40]},{"t":358.000014581639,"s":[7]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[162.125,87.75,0],"ix":2},"a":{"a":0,"k":[160,90,0],"ix":1},"s":{"a":0,"k":[105,108.889,100],"ix":6}},"ao":0,"sw":320,"sh":180,"sc":"#2d2d2d","ip":0,"op":900.000036657751,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":2,"nm":"Rectangle.png","cl":"png","refId":"image_8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1.022]},"o":{"x":[0.027],"y":[0.608]},"t":69,"s":[0]},{"i":{"x":[0],"y":[43.204]},"o":{"x":[0.167],"y":[0]},"t":83,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":348,"s":[100]},{"t":358.000014581639,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":69,"s":[132.5,-168.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[132.5,-105.5,0]}],"ix":2},"a":{"a":0,"k":[202.5,354,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":55.0000022401959,"op":955.000038897947,"st":55.0000022401959,"bm":0}],"markers":[{"tm":142.192505791619,"cm":"1","dr":0},{"tm":147.000005987433,"cm":"2","dr":0},{"tm":156.00000635401,"cm":"3","dr":0}]} diff --git a/DuckDuckGo/ImportPasswordsViewModel.swift b/DuckDuckGo/ImportPasswordsViewModel.swift index 480eb27ef4..46b0e20cc5 100644 --- a/DuckDuckGo/ImportPasswordsViewModel.swift +++ b/DuckDuckGo/ImportPasswordsViewModel.swift @@ -79,7 +79,7 @@ final class ImportPasswordsViewModel { /// Keeping track on whether or not either button was pressed on this screen /// so that a pixel can be fired if the user navigates away without taking any action - private (set) var buttonWasPressed: Bool = false + private(set) var buttonWasPressed: Bool = false func maxButtonWidth() -> CGFloat { let maxWidth = maxWidthFor(title1: ButtonType.getBrowser.title, title2: ButtonType.sync.title) diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist index a19b12c025..6d3941b0ca 100644 --- a/DuckDuckGo/Info.plist +++ b/DuckDuckGo/Info.plist @@ -240,6 +240,8 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSAdvertisingAttributionReportEndpoint + https://duckduckgo.com UIViewControllerBasedStatusBarAppearance diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index ca18ab0e12..edf27a7393 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -1381,14 +1381,18 @@ class MainViewController: UIViewController { private func makeBrokenSitePromptViewHostingController(event: UserBehaviorEvent) -> UIHostingController { let viewModel = BrokenSitePromptViewModel(onDidDismiss: { [weak self] in - self?.hideNotification() - self?.brokenSitePromptLimiter.didDismissToast() - self?.brokenSitePromptViewHostingController = nil + Task { @MainActor in + self?.hideNotification() + self?.brokenSitePromptLimiter.didDismissToast() + self?.brokenSitePromptViewHostingController = nil + } }, onDidSubmit: { [weak self] in - self?.segueToReportBrokenSite(entryPoint: .prompt(event.rawValue)) - self?.hideNotification() - self?.brokenSitePromptLimiter.didOpenReport() - self?.brokenSitePromptViewHostingController = nil + Task { @MainActor in + self?.segueToReportBrokenSite(entryPoint: .prompt(event.rawValue)) + self?.hideNotification() + self?.brokenSitePromptLimiter.didOpenReport() + self?.brokenSitePromptViewHostingController = nil + } }) return UIHostingController(rootView: BrokenSitePromptView(viewModel: viewModel), ignoreSafeArea: true) } diff --git a/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPicker.swift b/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPicker.swift new file mode 100644 index 0000000000..51e784fde0 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPicker.swift @@ -0,0 +1,186 @@ +// +// OnboardingAddressBarPositionPicker.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 SwiftUI + +struct OnboardingAddressBarPositionPicker: View { + @StateObject private var viewModel = OnboardingAddressBarPositionPickerViewModel() + + var body: some View { + VStack(spacing: 16.0) { + ForEach(viewModel.items, id: \.type) { item in + AddressBarPositionButton( + icon: item.icon, + title: AttributedString(item.title), + message: item.message, + isSelected: item.isSelected, + action: { + viewModel.setAddressBar(position: item.type) + } + ) + } + } + } +} + +// MARK: - Views + +private enum Metrics { + enum Button { + static let messageFont = Font.system(size: 15) + static let overlayRadius: CGFloat = 13.0 + static let overlayStroke: CGFloat = 1 + static let itemSpacing: CGFloat = 16.0 + } + enum Checkbox { + static let size: CGFloat = 24.0 + static let checkSize: CGSize = CGSize(width: 12, height: 10) + static let strokeInset = 0.75 + static let strokeWidth = 1.5 + } +} + +extension OnboardingAddressBarPositionPicker { + + struct AddressBarPositionButton: View { + @Environment(\.colorScheme) private var colorScheme + + let icon: ImageResource + let title: AttributedString + let message: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: Metrics.Button.itemSpacing) { + Image(icon) + + VStack(alignment: .leading) { + Text(title) + Text(message) + .font(Metrics.Button.messageFont) + .foregroundStyle(Color.secondary) + } + + Spacer() + + Checkbox(isSelected: isSelected) + } + } + .overlay { + RoundedRectangle(cornerRadius: Metrics.Button.overlayRadius) + .stroke(.blue, lineWidth: Metrics.Button.overlayStroke) + } + .buttonStyle(AddressBarPostionButtonStyle()) + } + + } + +} + +extension OnboardingAddressBarPositionPicker.AddressBarPositionButton { + + struct Checkbox: View { + @Environment(\.colorScheme) private var colorScheme + + let isSelected: Bool + + var body: some View { + Circle() + .frame(width: Metrics.Checkbox.size, height: Metrics.Checkbox.size) + .foregroundColor(foregroundColor) + .overlay { + selectionOverlay + } + } + + @ViewBuilder + private var selectionOverlay: some View { + if isSelected { + Image(.checkShape) + .resizable() + .frame(width: Metrics.Checkbox.checkSize.width, height: Metrics.Checkbox.checkSize.height) + .foregroundColor(.white) + } else { + Circle() + .inset(by: Metrics.Checkbox.strokeInset) + .stroke(.secondary, lineWidth: Metrics.Checkbox.strokeWidth) + } + } + + private var foregroundColor: Color { + switch (colorScheme, isSelected) { + case (.light, true), (.dark, true): + Color.init(designSystemColor: .accent) + case (.light, false): + .black.opacity(0.03) + case (.dark, false): + .white.opacity(0.06) + default: + .clear + } + } + } + +} + +// MARK: - Style + +private struct AddressBarPostionButtonStyle: ButtonStyle { + @Environment(\.colorScheme) private var colorScheme + + private let minHeight = 63.0 + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.leading) + .lineLimit(nil) + .padding() + .frame(minWidth: 0, maxWidth: .infinity, minHeight: minHeight) + .background(backgroundColor(configuration.isPressed)) + .cornerRadius(8) + .contentShape(Rectangle()) // Makes whole button area tappable, when there's no background + } + + private func foregroundColor(_ isPressed: Bool) -> Color { + switch (colorScheme, isPressed) { + case (.dark, false): + return .blue30 + case (.dark, true): + return .blue20 + case (_, false): + return .blueBase + case (_, true): + return .blue70 + } + } + + private func backgroundColor(_ isPressed: Bool) -> Color { + switch (colorScheme, isPressed) { + case (.light, true): + return .blueBase.opacity(0.2) + case (.dark, true): + return .blue30.opacity(0.2) + default: + return .clear + } + } +} diff --git a/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPickerViewModel.swift b/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPickerViewModel.swift new file mode 100644 index 0000000000..595fb5c061 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/AddressBarPositionPicker/OnboardingAddressBarPositionPickerViewModel.swift @@ -0,0 +1,104 @@ +// +// OnboardingAddressBarPositionPickerViewModel.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 Core + +final class OnboardingAddressBarPositionPickerViewModel: ObservableObject { + + struct DisplayModel { + let type: AddressBarPosition + let icon: ImageResource + let title: NSAttributedString + let message: String + let isSelected: Bool + } + + @Published private(set) var items: [DisplayModel] = [] + + private let addressBarPositionManager: AddressBarPositionManaging + + init(addressBarPositionManager: AddressBarPositionManaging = AppUserDefaults()) { + self.addressBarPositionManager = addressBarPositionManager + makeDisplayModels() + } + + func setAddressBar(position: AddressBarPosition) { + addressBarPositionManager.currentAddressBarPosition = position + makeDisplayModels() + } + + private func makeDisplayModels() { + items = AddressBarPosition.allCases.map { addressBarPosition in + let info = addressBarPosition.titleAndMessage + + return DisplayModel( + type: addressBarPosition, + icon: addressBarPosition.image, + title: info.title, + message: info.message, + isSelected: addressBarPositionManager.currentAddressBarPosition == addressBarPosition + ) + } + } +} + +// MARK: - AddressBarPositionManaging + +protocol AddressBarPositionManaging: AnyObject { + var currentAddressBarPosition: AddressBarPosition { get set } +} + +extension AppUserDefaults: AddressBarPositionManaging {} + +// MARK: - AddressBarPosition Helpers + +private extension AddressBarPosition { + + var titleAndMessage: (title: NSAttributedString, message: String) { + switch self { + case .top: + let firstPart = NSAttributedString(string: UserText.HighlightsOnboardingExperiment.AddressBarPosition.topTitle) + .withFont(UIFont.daxBodyBold()) + .withTextColor(UIColor.label) + let secondPart = NSAttributedString(string: UserText.HighlightsOnboardingExperiment.AddressBarPosition.defaultOption) + .withFont(UIFont.daxBodyRegular()) + .withTextColor(UIColor.secondaryLabel) + + return ( + firstPart + " " + secondPart, + UserText.HighlightsOnboardingExperiment.AddressBarPosition.topMessage + ) + case .bottom: + return ( + NSAttributedString(string: UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomTitle) + .withFont(UIFont.daxBodyBold()), + UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomMessage + ) + } + } + + var image: ImageResource { + switch self { + case .top: .addressBarTop + case .bottom: .addressBarBottom + } + } + +} diff --git a/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift b/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift new file mode 100644 index 0000000000..8ff955f2d0 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPicker.swift @@ -0,0 +1,73 @@ +// +// AppIconPicker.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 SwiftUI +import DuckUI + +private enum Metrics { + static let cornerRadius: CGFloat = 13.0 + static let iconSize: CGFloat = 56.0 + static let spacing: CGFloat = 16.0 + static let strokeFrameSize: CGFloat = 60 + static let strokeWidth: CGFloat = 3 + static let strokeInset: CGFloat = 1.5 +} + +struct AppIconPicker: View { + @Environment(\.colorScheme) private var color + + @StateObject private var viewModel = AppIconPickerViewModel() + + let layout = [GridItem(.adaptive(minimum: Metrics.iconSize, maximum: Metrics.iconSize), spacing: Metrics.spacing)] + + var body: some View { + LazyVGrid(columns: layout, spacing: Metrics.spacing) { + ForEach(viewModel.items, id: \.icon) { item in + Image(uiImage: item.icon.mediumImage ?? UIImage()) + .resizable() + .frame(width: Metrics.iconSize, height: Metrics.iconSize) + .cornerRadius(Metrics.cornerRadius) + .overlay { + strokeOverlay(isSelected: item.isSelected) + } + .onTapGesture { + viewModel.changeApp(icon: item.icon) + } + } + } + } + + @ViewBuilder + private func strokeOverlay(isSelected: Bool) -> some View { + if isSelected { + RoundedRectangle(cornerRadius: Metrics.cornerRadius) + .foregroundColor(.clear) + .frame(width: Metrics.strokeFrameSize, height: Metrics.strokeFrameSize) + .overlay( + RoundedRectangle(cornerRadius: Metrics.cornerRadius) + .inset(by: -Metrics.strokeInset) + .stroke(.blue, lineWidth: Metrics.strokeWidth) + ) + } + } +} + +#Preview { + AppIconPicker() +} diff --git a/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPickerViewModel.swift b/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPickerViewModel.swift new file mode 100644 index 0000000000..ceaebee301 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/AppIconPicker/AppIconPickerViewModel.swift @@ -0,0 +1,64 @@ +// +// AppIconPickerViewModel.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 + +@MainActor +final class AppIconPickerViewModel: ObservableObject { + + struct DisplayModel { + let icon: AppIcon + let isSelected: Bool + } + + @Published private(set) var items: [DisplayModel] = [] + + private let appIconManager: AppIconManaging + + init(appIconManager: AppIconManaging = AppIconManager.shared) { + self.appIconManager = appIconManager + items = makeDisplayModels() + } + + func changeApp(icon: AppIcon) { + appIconManager.changeAppIcon(icon) { [weak self] error in + guard let self, error == nil else { return } + items = makeDisplayModels() + } + } + + private func makeDisplayModels() -> [DisplayModel] { + AppIcon.allCases.map { appIcon in + DisplayModel(icon: appIcon, isSelected: appIconManager.appIcon == appIcon) + } + } +} + +protocol AppIconManaging { + var appIcon: AppIcon { get } + func changeAppIcon(_ appIcon: AppIcon, completionHandler: ((Error?) -> Void)?) +} + +extension AppIconManaging { + func changeAppIcon(_ appIcon: AppIcon) { + changeAppIcon(appIcon, completionHandler: nil) + } +} + +extension AppIconManager: AppIconManaging {} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift b/DuckDuckGo/OnboardingExperiment/Background/OnboardingBackground.swift similarity index 78% rename from DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift rename to DuckDuckGo/OnboardingExperiment/Background/OnboardingBackground.swift index 716c0b210b..7e5b6fb531 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingBackground.swift +++ b/DuckDuckGo/OnboardingExperiment/Background/OnboardingBackground.swift @@ -18,9 +18,9 @@ // import SwiftUI -import Onboarding struct OnboardingBackground: View { + @Environment(\.onboardingGradientType) private var gradientType @Environment(\.verticalSizeClass) private var vSizeClass @Environment(\.horizontalSizeClass) private var hSizeClass @Environment(\.colorScheme) private var colorScheme @@ -35,7 +35,7 @@ struct OnboardingBackground: View { .opacity(colorScheme == .light ? 0.5 : 0.3) .frame(width: proxy.size.width, height: proxy.size.height, alignment: alignment) .background( - OnboardingGradient() + OnboardingGradientView(type: gradientType) .ignoresSafeArea() ) } @@ -46,13 +46,26 @@ private enum Metrics { static let imageCentering = MetricBuilder(iPhone: .bottomLeading, iPad: .center) } - #Preview("Light Mode") { OnboardingBackground() + .onboardingGradient(.default) .preferredColorScheme(.light) } #Preview("Dark Mode") { OnboardingBackground() + .onboardingGradient(.default) + .preferredColorScheme(.dark) +} + +#Preview("Light Mode - Highlights") { + OnboardingBackground() + .onboardingGradient(.highlights) + .preferredColorScheme(.light) +} + +#Preview("Dark Mode - Highlights") { + OnboardingBackground() + .onboardingGradient(.highlights) .preferredColorScheme(.dark) } diff --git a/DuckDuckGo/OnboardingExperiment/Background/OnboardingGradient.swift b/DuckDuckGo/OnboardingExperiment/Background/OnboardingGradient.swift new file mode 100644 index 0000000000..c5b6fff5a6 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/Background/OnboardingGradient.swift @@ -0,0 +1,79 @@ +// +// OnboardingGradient.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 SwiftUI +import Onboarding + +struct OnboardingGradientView: View { + @Environment(\.colorScheme) private var colorScheme + + private let type: OnboardingGradientType + + init(type: OnboardingGradientType) { + self.type = type + } + + var body: some View { + switch (type, colorScheme) { + case (.default, .light): + linearLightGradient + case (.default, .dark): + linearDarkGradient + case (.highlights, _): + // If highlights experiment use new common gradient for iOS and macOS + OnboardingGradient() + @unknown default: + linearLightGradient + } + } + + private var linearLightGradient: some View { + gradient(colorStops: [ + .init(color: Color(red: 1, green: 0.9, blue: 0.87), location: 0.00), + .init(color: Color(red: 0.99, green: 0.89, blue: 0.87), location: 0.28), + .init(color: Color(red: 0.99, green: 0.89, blue: 0.87), location: 0.46), + .init(color: Color(red: 0.96, green: 0.87, blue: 0.87), location: 0.72), + .init(color: Color(red: 0.9, green: 0.84, blue: 0.92), location: 1.00), + ]) + } + + private var linearDarkGradient: some View { + gradient(colorStops: [ + .init(color: Color(red: 0.29, green: 0.19, blue: 0.25), location: 0.00), + .init(color: Color(red: 0.35, green: 0.23, blue: 0.32), location: 0.28), + .init(color: Color(red: 0.37, green: 0.25, blue: 0.38), location: 0.46), + .init(color: Color(red: 0.2, green: 0.15, blue: 0.32), location: 0.72), + .init(color: Color(red: 0.16, green: 0.15, blue: 0.34), location: 1.00), + ]) + } + + private func gradient(colorStops: [SwiftUI.Gradient.Stop]) -> some View { + LinearGradient( + stops: colorStops, + startPoint: UnitPoint(x: 0.5, y: 0), + endPoint: UnitPoint(x: 0.5, y: 1) + ) + } + +} + +enum OnboardingGradientType { + case `default` + case highlights +} diff --git a/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift b/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift index 0d60655d97..6f4f855868 100644 --- a/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift +++ b/DuckDuckGo/OnboardingExperiment/BrowsersComparison/BrowsersComparisonModel.swift @@ -117,17 +117,28 @@ extension BrowsersComparisonModel.PrivacyFeature { case blockCreepyAds case eraseBrowsingData + // Remove it once Highlights experiment finishes + static var onboardingManager: OnboardingHighlightsManaging = OnboardingManager() + var title: String { switch self { case .privateSearch: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.privateSearch case .blockThirdPartyTrackers: + Self.onboardingManager.isOnboardingHighlightsEnabled ? + UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.trackerBlockers : UserText.DaxOnboardingExperiment.BrowsersComparison.Features.trackerBlockers case .blockCookiePopups: + Self.onboardingManager.isOnboardingHighlightsEnabled ? + UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.cookiePopups: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.cookiePopups case .blockCreepyAds: + Self.onboardingManager.isOnboardingHighlightsEnabled ? + UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.creepyAds : UserText.DaxOnboardingExperiment.BrowsersComparison.Features.creepyAds case .eraseBrowsingData: + Self.onboardingManager.isOnboardingHighlightsEnabled ? + UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData } } diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift index 0337ac81ab..ea4362b6f1 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/ContextualOnboardingDialogs.swift @@ -24,7 +24,7 @@ import DuckUI struct OnboardingTrySearchDialog: View { let title = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASearchTitle - let message = NSAttributedString(string: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASearchMessage) + let message: String let viewModel: OnboardingSearchSuggestionsViewModel var body: some View { @@ -33,7 +33,7 @@ struct OnboardingTrySearchDialog: View { ContextualDaxDialogContent( title: title, titleFont: Font(UIFont.daxTitle3()), - message: message, + message: NSAttributedString(string: message), list: viewModel.itemsList, listAction: viewModel.listItemPressed ) @@ -95,8 +95,8 @@ struct OnboardingFireButtonDialogContent: View { } struct OnboardingFirstSearchDoneDialog: View { - let message = NSAttributedString(string: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage) let cta = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingGotItButton + let message: String @State private var showNextScreen: Bool = false @@ -112,7 +112,7 @@ struct OnboardingFirstSearchDoneDialog: View { OnboardingTryVisitingSiteDialogContent(viewModel: viewModel) } else { ContextualDaxDialogContent( - message: message, + message: NSAttributedString(string: message), customActionView: AnyView( OnboardingCTAButton(title: cta) { gotItAction() @@ -185,7 +185,7 @@ struct OnboardingTrackersDoneDialog: View { struct OnboardingFinalDialog: View { let title = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenTitle - let message = NSAttributedString(string: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage) + let message: String let cta = UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenButton let highFiveAction: () -> Void @@ -196,7 +196,7 @@ struct OnboardingFinalDialog: View { ContextualDaxDialogContent( title: title, titleFont: Font(UIFont.daxTitle3()), - message: message, + message: NSAttributedString(string: message), customActionView: AnyView( OnboardingCTAButton( title: cta, @@ -226,7 +226,7 @@ struct OnboardingCTAButton: View { // MARK: - Preview #Preview("Try Search") { - OnboardingTrySearchDialog(viewModel: OnboardingSearchSuggestionsViewModel(suggestedSearchesProvider: OnboardingSuggestedSearchesProvider(), pixelReporter: OnboardingPixelReporter())) + OnboardingTrySearchDialog(message: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASearchMessage, viewModel: OnboardingSearchSuggestionsViewModel(suggestedSearchesProvider: OnboardingSuggestedSearchesProvider(), pixelReporter: OnboardingPixelReporter())) .padding() } @@ -248,12 +248,12 @@ struct OnboardingCTAButton: View { } #Preview("First Search Dialog") { - OnboardingFirstSearchDoneDialog(shouldFollowUp: true, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter()), gotItAction: {}) + OnboardingFirstSearchDoneDialog(message: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage, shouldFollowUp: true, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter()), gotItAction: {}) .padding() } #Preview("Final Dialog") { - OnboardingFinalDialog(highFiveAction: {}) + OnboardingFinalDialog(message: UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage, highFiveAction: {}) .padding() } diff --git a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift index 2681a5f0de..7f6f068844 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualDaxDialogs/NewTabDaxDialogFactory.swift @@ -30,15 +30,22 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { private var delegate: OnboardingNavigationDelegate? private let contextualOnboardingLogic: ContextualOnboardingLogic private let onboardingPixelReporter: OnboardingPixelReporting + private let onboardingManager: OnboardingHighlightsManaging + + private var gradientType: OnboardingGradientType { + onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default + } init( delegate: OnboardingNavigationDelegate?, contextualOnboardingLogic: ContextualOnboardingLogic, - onboardingPixelReporter: OnboardingPixelReporting + onboardingPixelReporter: OnboardingPixelReporting, + onboardingManager: OnboardingHighlightsManaging = OnboardingManager() ) { self.delegate = delegate self.contextualOnboardingLogic = contextualOnboardingLogic self.onboardingPixelReporter = onboardingPixelReporter + self.onboardingManager = onboardingManager } @ViewBuilder @@ -54,17 +61,17 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { createFinalDialog(onDismiss: onDismiss) default: EmptyView() - } } private func createInitialDialog() -> some View { let viewModel = OnboardingSearchSuggestionsViewModel(suggestedSearchesProvider: OnboardingSuggestedSearchesProvider(), delegate: delegate, pixelReporter: onboardingPixelReporter) + let message = onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingTryASearchMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingTryASearchMessage return FadeInView { - OnboardingTrySearchDialog(viewModel: viewModel) + OnboardingTrySearchDialog(message: message, viewModel: viewModel) .onboardingDaxDialogStyle() } - .onboardingContextualBackgroundStyle() + .onboardingContextualBackgroundStyle(background: .illustratedGradient(gradientType)) .onFirstAppear { [weak self] in self?.onboardingPixelReporter.trackScreenImpression(event: .onboardingContextualTrySearchUnique) } @@ -76,7 +83,7 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { OnboardingTryVisitingSiteDialog(logoPosition: .top, viewModel: viewModel) .onboardingDaxDialogStyle() } - .onboardingContextualBackgroundStyle() + .onboardingContextualBackgroundStyle(background: .illustratedGradient(gradientType)) .onFirstAppear { [weak self] in self?.onboardingPixelReporter.trackScreenImpression(event: .onboardingContextualTryVisitSiteUnique) } @@ -92,13 +99,15 @@ final class NewTabDaxDialogFactory: NewTabDaxDialogProvider { } private func createFinalDialog(onDismiss: @escaping () -> Void) -> some View { - FadeInView { - OnboardingFinalDialog(highFiveAction: { + let message = onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage + + return FadeInView { + OnboardingFinalDialog(message: message, highFiveAction: { onDismiss() }) .onboardingDaxDialogStyle() } - .onboardingContextualBackgroundStyle() + .onboardingContextualBackgroundStyle(background: .illustratedGradient(gradientType)) .onFirstAppear { [weak self] in self?.contextualOnboardingLogic.setFinalOnboardingDialogSeen() self?.onboardingPixelReporter.trackScreenImpression(event: .daxDialogsEndOfJourneyNewTabUnique) diff --git a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift index 60dfeefccf..04fa68f5df 100644 --- a/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift +++ b/DuckDuckGo/OnboardingExperiment/ContextualOnboarding/ContextualDaxDialogsFactory.swift @@ -48,17 +48,24 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { private let contextualOnboardingSettings: ContextualOnboardingSettings private let contextualOnboardingPixelReporter: OnboardingPixelReporting private let contextualOnboardingSiteSuggestionsProvider: OnboardingSuggestionsItemsProviding + private let onboardingManager: OnboardingHighlightsManaging + + private var gradientType: OnboardingGradientType { + onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default + } init( contextualOnboardingLogic: ContextualOnboardingLogic, contextualOnboardingSettings: ContextualOnboardingSettings = DefaultDaxDialogsSettings(), contextualOnboardingPixelReporter: OnboardingPixelReporting, - contextualOnboardingSiteSuggestionsProvider: OnboardingSuggestionsItemsProviding = OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) + contextualOnboardingSiteSuggestionsProvider: OnboardingSuggestionsItemsProviding = OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), + onboardingManager: OnboardingHighlightsManaging = OnboardingManager() ) { self.contextualOnboardingSettings = contextualOnboardingSettings self.contextualOnboardingLogic = contextualOnboardingLogic self.contextualOnboardingPixelReporter = contextualOnboardingPixelReporter self.contextualOnboardingSiteSuggestionsProvider = contextualOnboardingSiteSuggestionsProvider + self.onboardingManager = onboardingManager } func makeView(for spec: DaxDialogs.BrowsingSpec, delegate: ContextualOnboardingDelegate, onSizeUpdate: @escaping () -> Void) -> UIHostingController { @@ -92,7 +99,7 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { let viewWithBackground = rootView .onboardingDaxDialogStyle() - .onboardingContextualBackgroundStyle() + .onboardingContextualBackgroundStyle(background: .gradientOnly(gradientType)) let hostingController = UIHostingController(rootView: AnyView(viewWithBackground)) if #available(iOS 16.0, *) { hostingController.sizingOptions = [.intrinsicContentSize] @@ -122,7 +129,9 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { } } - return OnboardingFirstSearchDoneDialog(shouldFollowUp: shouldFollowUpToWebsiteSearch, viewModel: viewModel, gotItAction: gotItAction) + let message = onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFirstSearchDoneMessage + + return OnboardingFirstSearchDoneDialog(message: message, shouldFollowUp: shouldFollowUpToWebsiteSearch, viewModel: viewModel, gotItAction: gotItAction) .onFirstAppear { [weak self] in self?.contextualOnboardingPixelReporter.trackScreenImpression(event: afterSearchPixelEvent) } @@ -164,7 +173,9 @@ final class ExperimentContextualDaxDialogsFactory: ContextualDaxDialogsFactory { } private func endOfJourneyDialog(delegate: ContextualOnboardingDelegate, pixelName: Pixel.Event) -> some View { - OnboardingFinalDialog(highFiveAction: { [weak delegate] in + let message = onboardingManager.isOnboardingHighlightsEnabled ? UserText.HighlightsOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage : UserText.DaxOnboardingExperiment.ContextualOnboarding.onboardingFinalScreenMessage + + return OnboardingFinalDialog(message: message, highFiveAction: { [weak delegate] in delegate?.didTapDismissContextualOnboardingAction() }) .onFirstAppear { [weak self] in diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel+Copy.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel+Copy.swift new file mode 100644 index 0000000000..5e2e713a7e --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel+Copy.swift @@ -0,0 +1,52 @@ +// +// OnboardingIntroViewModel+Copy.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 + +extension OnboardingIntroViewModel { + struct Copy { + let introTitle: String + let browserComparisonTitle: String + let trackerBlockers: String + let cookiePopups: String + let creepyAds: String + let eraseBrowsingData: String + } +} + +extension OnboardingIntroViewModel.Copy { + + static let `default` = OnboardingIntroViewModel.Copy( + introTitle: UserText.DaxOnboardingExperiment.Intro.title, + browserComparisonTitle: UserText.DaxOnboardingExperiment.BrowsersComparison.title, + trackerBlockers: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.trackerBlockers, + cookiePopups: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.cookiePopups, + creepyAds: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.creepyAds, + eraseBrowsingData: UserText.DaxOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData + ) + + static let highlights = OnboardingIntroViewModel.Copy( + introTitle: UserText.HighlightsOnboardingExperiment.Intro.title, + browserComparisonTitle: UserText.HighlightsOnboardingExperiment.BrowsersComparison.title, + trackerBlockers: UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.trackerBlockers, + cookiePopups: UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.cookiePopups, + creepyAds: UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.creepyAds, + eraseBrowsingData: UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData + ) +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift index ffda3bda2b..419ea7daf1 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingIntroViewModel.swift @@ -19,33 +19,49 @@ import Foundation import Core +import Onboarding import class UIKit.UIApplication final class OnboardingIntroViewModel: ObservableObject { @Published private(set) var state: OnboardingView.ViewState = .landing + let copy: Copy + let gradientType: OnboardingGradientType var onCompletingOnboardingIntro: (() -> Void)? + private var introSteps: [OnboardingIntroStep] + private let pixelReporter: OnboardingIntroPixelReporting private let onboardingManager: OnboardingHighlightsManaging + private let isIpad: Bool private let urlOpener: URLOpener init( pixelReporter: OnboardingIntroPixelReporting, onboardingManager: OnboardingHighlightsManaging = OnboardingManager(), + isIpad: Bool = UIDevice.current.userInterfaceIdiom == .pad, urlOpener: URLOpener = UIApplication.shared ) { self.pixelReporter = pixelReporter self.onboardingManager = onboardingManager + self.isIpad = isIpad self.urlOpener = urlOpener + introSteps = if onboardingManager.isOnboardingHighlightsEnabled { + isIpad ? OnboardingIntroStep.highlightsIPadFlow : OnboardingIntroStep.highlightsIPhoneFlow + } else { + OnboardingIntroStep.defaultFlow + } + + copy = onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default + gradientType = onboardingManager.isOnboardingHighlightsEnabled ? .highlights : .default } func onAppear() { - state = .onboarding(.startOnboardingDialog) + state = makeViewState(for: .introDialog) pixelReporter.trackOnboardingIntroImpression() } func startOnboardingAction() { - state = .onboarding(.browsersComparisonDialog) + state = makeViewState(for: .browserComparison) pixelReporter.trackBrowserComparisonImpression() } @@ -63,7 +79,14 @@ final class OnboardingIntroViewModel: ObservableObject { } func appIconPickerContinueAction() { - // TODO: Remove below and implement proper logic + if isIpad { + onCompletingOnboardingIntro?() + } else { + state = makeViewState(for: .addressBarPositionSelection) + } + } + + func selectAddressBarPositionAction() { onCompletingOnboardingIntro?() } @@ -73,12 +96,53 @@ final class OnboardingIntroViewModel: ObservableObject { private extension OnboardingIntroViewModel { + func makeViewState(for introStep: OnboardingIntroStep) -> OnboardingView.ViewState { + + func stepInfo() -> OnboardingView.ViewState.Intro.StepInfo { + guard + let currentStepIndex = introSteps.firstIndex(of: introStep), + onboardingManager.isOnboardingHighlightsEnabled + else { + return .hidden + } + + // Remove startOnboardingDialog from the count of total steps since we don't show the progress for that step. + return OnboardingView.ViewState.Intro.StepInfo(currentStep: currentStepIndex, totalSteps: introSteps.count - 1) + } + + let viewState = switch introStep { + case .introDialog: + OnboardingView.ViewState.onboarding(.init(type: .startOnboardingDialog, step: .hidden)) + case .browserComparison: + OnboardingView.ViewState.onboarding(.init(type: .browsersComparisonDialog, step: stepInfo())) + case .appIconSelection: + OnboardingView.ViewState.onboarding(.init(type: .chooseAppIconDialog, step: stepInfo())) + case .addressBarPositionSelection: + OnboardingView.ViewState.onboarding(.init(type: .chooseAddressBarPositionDialog, step: stepInfo())) + } + + return viewState + } + func handleSetDefaultBrowserAction() { if onboardingManager.isOnboardingHighlightsEnabled { - state = .onboarding(.chooseAppIconDialog) + state = makeViewState(for: .appIconSelection) } else { onCompletingOnboardingIntro?() } } } + +// MARK: - OnboardingIntroStep + +private enum OnboardingIntroStep { + case introDialog + case browserComparison + case appIconSelection + case addressBarPositionSelection + + static let defaultFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison] + static let highlightsIPhoneFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection, .addressBarPositionSelection] + static let highlightsIPadFlow: [OnboardingIntroStep] = [.introDialog, .browserComparison, .appIconSelection] +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AddressBarPositionContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AddressBarPositionContent.swift new file mode 100644 index 0000000000..5ef61b133c --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AddressBarPositionContent.swift @@ -0,0 +1,79 @@ +// +// OnboardingView+AddressBarPositionContent.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 SwiftUI +import DuckUI +import Onboarding + +private enum Metrics { + static let titleFont = Font.system(size: 20, weight: .semibold) + static let messageFont = Font.system(size: 16) +} + +extension OnboardingView { + + struct AddressBarPositionContentState { + var animateTitle = true + var showContent = false + } + + struct AddressBarPositionContent: View { + + private var animateTitle: Binding + private var showContent: Binding + private let action: () -> Void + + init( + animateTitle: Binding = .constant(true), + showContent: Binding = .constant(true), + action: @escaping () -> Void + ) { + self.animateTitle = animateTitle + self.showContent = showContent + self.action = action + } + + var body: some View { + VStack(spacing: 16.0) { + AnimatableTypingText(UserText.HighlightsOnboardingExperiment.AddressBarPosition.title, startAnimating: animateTitle) { + showContent.wrappedValue = true + } + .foregroundColor(.primary) + .font(Metrics.titleFont) + + VStack(spacing: 24) { + OnboardingAddressBarPositionPicker() + + Button(action: action) { + Text(verbatim: UserText.HighlightsOnboardingExperiment.AddressBarPosition.cta) + } + .buttonStyle(PrimaryButtonStyle()) + } + .visibility(showContent.wrappedValue ? .visible : .invisible) + } + } + } + +} + +// MARK: - Preview + +#Preview { + OnboardingView.AddressBarPositionContent(action: {}) +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift new file mode 100644 index 0000000000..3d2f8e19bb --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+AppIconPickerContent.swift @@ -0,0 +1,88 @@ +// +// OnboardingView+AppIconPickerContent.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 SwiftUI +import DuckUI +import Onboarding + +extension OnboardingView { + + struct AppIconPickerContentState { + var animateTitle = true + var animateMessage = false + var showContent = false + } + + struct AppIconPickerContent: View { + + private var animateTitle: Binding + private var animateMessage: Binding + private var showContent: Binding + private let action: () -> Void + + init( + animateTitle: Binding = .constant(true), + animateMessage: Binding = .constant(true), + showContent: Binding = .constant(false), + action: @escaping () -> Void + ) { + self.animateTitle = animateTitle + self.animateMessage = animateMessage + self.showContent = showContent + self.action = action + } + + var body: some View { + VStack(spacing: 16.0) { + AnimatableTypingText(UserText.HighlightsOnboardingExperiment.AppIconSelection.title, startAnimating: animateTitle) { + animateMessage.wrappedValue = true + } + .foregroundColor(.primary) + .font(Metrics.titleFont) + + AnimatableTypingText(UserText.HighlightsOnboardingExperiment.AppIconSelection.message, startAnimating: animateMessage) { + withAnimation { + showContent.wrappedValue = true + } + } + .foregroundColor(.primary) + .font(Metrics.messageFont) + + VStack(spacing: 24) { + AppIconPicker() + .offset(x: Metrics.pickerLeadingOffset) // Remove left padding for the first item + + Button(action: action) { + Text(UserText.HighlightsOnboardingExperiment.AppIconSelection.cta) + } + .buttonStyle(PrimaryButtonStyle()) + } + .visibility(showContent.wrappedValue ? .visible : .invisible) + } + } + + } + +} + +private enum Metrics { + static let titleFont = Font.system(size: 20, weight: .semibold) + static let messageFont = Font.system(size: 16) + static let pickerLeadingOffset: CGFloat = -20 +} diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift index bd6611c0df..0772d9e3b8 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+BrowsersComparisonContent.swift @@ -25,12 +25,20 @@ extension OnboardingView { struct BrowsersComparisonContent: View { + private let title: String private var animateText: Binding private var showContent: Binding private let setAsDefaultBrowserAction: () -> Void private let cancelAction: () -> Void - init(animateText: Binding = .constant(true), showContent: Binding = .constant(false), setAsDefaultBrowserAction: @escaping () -> Void, cancelAction: @escaping () -> Void) { + init( + title: String, + animateText: Binding = .constant(true), + showContent: Binding = .constant(false), + setAsDefaultBrowserAction: @escaping () -> Void, + cancelAction: @escaping () -> Void + ) { + self.title = title self.animateText = animateText self.showContent = showContent self.setAsDefaultBrowserAction = setAsDefaultBrowserAction @@ -39,7 +47,7 @@ extension OnboardingView { var body: some View { VStack(spacing: 16.0) { - AnimatableTypingText(UserText.DaxOnboardingExperiment.BrowsersComparison.title, startAnimating: animateText) { + AnimatableTypingText(title, startAnimating: animateText) { withAnimation { showContent.wrappedValue = true } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift index 5652d6237c..430be926ea 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView+IntroDialogContent.swift @@ -25,11 +25,13 @@ extension OnboardingView { struct IntroDialogContent: View { + private let title: String private var animateText: Binding private var showCTA: Binding private let action: () -> Void - init(animateText: Binding = .constant(true), showCTA: Binding = .constant(false), action: @escaping () -> Void) { + init(title: String, animateText: Binding = .constant(true), showCTA: Binding = .constant(false), action: @escaping () -> Void) { + self.title = title self.animateText = animateText self.showCTA = showCTA self.action = action @@ -37,7 +39,7 @@ extension OnboardingView { var body: some View { VStack(spacing: 24.0) { - AnimatableTypingText(UserText.DaxOnboardingExperiment.Intro.title, startAnimating: animateText) { + AnimatableTypingText(title) { withAnimation { showCTA.wrappedValue = true } diff --git a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift index 2af2121b7a..cd834e55b2 100644 --- a/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift +++ b/DuckDuckGo/OnboardingExperiment/OnboardingIntro/OnboardingView.swift @@ -38,6 +38,9 @@ struct OnboardingView: View { @State private var showComparisonButton = false @State private var animateComparisonText = false + @State private var appIconPickerContentState = AppIconPickerContentState() + @State private var addressBarPositionContentState = AddressBarPositionContentState() + init(model: OnboardingIntroViewModel) { self.model = model } @@ -53,6 +56,7 @@ struct OnboardingView: View { onboardingDialogView(state: viewState) } } + .onboardingGradient(model.gradientType) } private func onboardingDialogView(state: ViewState.Intro) -> some View { @@ -64,30 +68,37 @@ struct OnboardingView: View { showDialogBox: $showDaxDialogBox, onTapGesture: { withAnimation { - switch model.state { - case .onboarding(.startOnboardingDialog): + switch model.state.intro?.type { + case .startOnboardingDialog: showIntroButton = true animateIntroText = false - case .onboarding(.browsersComparisonDialog): + case .browsersComparisonDialog: showComparisonButton = true animateComparisonText = false + case .chooseAppIconDialog: + appIconPickerContentState.animateTitle = false + appIconPickerContentState.animateMessage = false + appIconPickerContentState.showContent = true default: break } } }, content: { VStack { - switch state { + switch state.type { case .startOnboardingDialog: introView case .browsersComparisonDialog: browsersComparisonView case .chooseAppIconDialog: appIconPickerView + case .chooseAddressBarPositionDialog: + addressBarPreferenceSelectionView } } } ) + .onboardingProgressIndicator(currentStep: state.step.currentStep, totalSteps: state.step.totalSteps) } .frame(width: geometry.size.width, alignment: .center) .offset(y: geometry.size.height * Metrics.dialogVerticalOffsetPercentage.build(v: verticalSizeClass, h: horizontalSizeClass)) @@ -115,7 +126,11 @@ struct OnboardingView: View { } private var introView: some View { - IntroDialogContent(animateText: $animateIntroText, showCTA: $showIntroButton) { + IntroDialogContent( + title: model.copy.introTitle, + animateText: $animateIntroText, + showCTA: $showIntroButton + ) { animateBrowserComparisonViewState() } .onboardingDaxDialogStyle() @@ -124,6 +139,7 @@ struct OnboardingView: View { private var browsersComparisonView: some View { BrowsersComparisonContent( + title: model.copy.browserComparisonTitle, animateText: $animateComparisonText, showContent: $showComparisonButton, setAsDefaultBrowserAction: { @@ -136,14 +152,21 @@ struct OnboardingView: View { } private var appIconPickerView: some View { - // TODO: Implement AppIconPicker - VStack(spacing: 30) { - Text(verbatim: "Choose App Icon") + AppIconPickerContent( + animateTitle: $appIconPickerContentState.animateTitle, + animateMessage: $appIconPickerContentState.animateMessage, + showContent: $appIconPickerContentState.showContent, + action: model.appIconPickerContinueAction + ) + .onboardingDaxDialogStyle() + } - Button(action: model.appIconPickerContinueAction) { - Text(verbatim: "Continue") - } - } + private var addressBarPreferenceSelectionView: some View { + AddressBarPositionContent( + animateTitle: $addressBarPositionContentState.animateTitle, + showContent: $addressBarPositionContentState.showContent, + action: model.selectAddressBarPositionAction + ) .onboardingDaxDialogStyle() } @@ -181,18 +204,44 @@ extension OnboardingView { enum ViewState: Equatable { case landing case onboarding(Intro) + + var intro: Intro? { + switch self { + case .landing: + return nil + case let .onboarding(intro): + return intro + } + } } } extension OnboardingView.ViewState { + + struct Intro: Equatable { + let type: IntroType + let step: StepInfo + } - enum Intro: Equatable { +} + +extension OnboardingView.ViewState.Intro { + + enum IntroType: Equatable { case startOnboardingDialog case browsersComparisonDialog case chooseAppIconDialog + case chooseAddressBarPositionDialog } - + + struct StepInfo: Equatable { + let currentStep: Int + let totalSteps: Int + + static let hidden = StepInfo(currentStep: 0, totalSteps: 0) + } + } // MARK: - Metrics @@ -202,6 +251,24 @@ private enum Metrics { static let daxDialogVisibilityDelay: TimeInterval = 0.5 static let comparisonChartAnimationDuration = 0.25 static let dialogVerticalOffsetPercentage = MetricBuilder(value: 0.1).smallIphone(0.01) + static let progressBarTrailingPadding: CGFloat = 16.0 + static let progressBarTopPadding: CGFloat = 12.0 +} + +// MARK: - Helpers + +private extension View { + + func onboardingProgressIndicator(currentStep: Int, totalSteps: Int) -> some View { + overlay(alignment: .topTrailing) { + OnboardingProgressIndicator(stepInfo: .init(currentStep: currentStep, totalSteps: totalSteps)) + .padding(.trailing, Metrics.progressBarTrailingPadding) + .padding(.top, Metrics.progressBarTopPadding) + .transition(.identity) + .visibility(totalSteps == 0 ? .invisible : .visible) + } + } + } // MARK: - Preview diff --git a/DuckDuckGo/OnboardingExperiment/ProgressBarView.swift b/DuckDuckGo/OnboardingExperiment/ProgressBarView.swift new file mode 100644 index 0000000000..cebcc13db3 --- /dev/null +++ b/DuckDuckGo/OnboardingExperiment/ProgressBarView.swift @@ -0,0 +1,159 @@ +// +// ProgressBarView.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 SwiftUI + +struct OnboardingProgressIndicator: View { + + struct StepInfo { + let currentStep: Int + let totalSteps: Int + + fileprivate var percentage: Double { + guard totalSteps > 0 else { return 0 } + return Double(currentStep) / Double(totalSteps) * 100 + } + } + + let stepInfo: StepInfo + + var body: some View { + VStack(spacing: OnboardingProgressMetrics.verticalSpacing) { + HStack { + Spacer() + Text("\(stepInfo.currentStep) / \(stepInfo.totalSteps)") + .onboardingProgressTitleStyle() + .padding(.trailing, OnboardingProgressMetrics.textPadding) + } + ProgressBarView(progress: stepInfo.percentage) + .frame(width: OnboardingProgressMetrics.progressBarSize.width, height: OnboardingProgressMetrics.progressBarSize.height) + } + .fixedSize() + } +} + +private enum OnboardingProgressMetrics { + static let verticalSpacing: CGFloat = 8 + static let textPadding: CGFloat = 4 + static let progressBarSize = CGSize(width: 64, height: 4) +} + +struct ProgressBarView: View { + @Environment(\.colorScheme) private var colorScheme + + let progress: Double + + var body: some View { + Capsule() + .foregroundStyle(backgroundColor) + .overlay( + GeometryReader { proxy in + ProgressBarGradient() + .clipShape(Capsule().inset(by: ProgressBarMetrics.strokeWidth / 2)) + .frame(width: progress * proxy.size.width / 100) + .animation(.easeInOut, value: progress) + } + ) + .overlay( + Capsule() + .stroke(borderColor, lineWidth: ProgressBarMetrics.strokeWidth) + ) + } + + private var backgroundColor: Color { + colorScheme == .light ? ProgressBarMetrics.backgroundLight : ProgressBarMetrics.backgroundDark + } + + private var borderColor: Color { + colorScheme == .light ? ProgressBarMetrics.borderLight : ProgressBarMetrics.borderDark + } + +} + +private enum ProgressBarMetrics { + static let backgroundLight: Color = .black.opacity(0.06) + static let borderLight: Color = .black.opacity(0.18) + static let backgroundDark: Color = .white.opacity(0.09) + static let borderDark: Color = .white.opacity(0.18) + static let strokeWidth: CGFloat = 1 +} + +struct ProgressBarGradient: View { + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + let colors: [Color] + switch colorScheme { + case .light: + colors = lightGradientColors + case .dark: + colors = darkGradientColors + @unknown default: + colors = lightGradientColors + } + + return LinearGradient( + colors: colors, + startPoint: .leading, + endPoint: .trailing + ) + } + + private var lightGradientColors: [Color] { + [ + .init(0x3969EF, alpha: 1.0), + .init(0x6B4EBA, alpha: 1.0), + .init(0xDE5833, alpha: 1.0), + ] + } + + private var darkGradientColors: [Color] { + [ + .init(0x3969EF, alpha: 1.0), + .init(0x6B4EBA, alpha: 1.0), + .init(0xDE5833, alpha: 1.0), + ] + } +} + +#Preview("Onboarding Progress Indicator") { + struct PreviewWrapper: View { + @State var stepInfo = OnboardingProgressIndicator.StepInfo(currentStep: 1, totalSteps: 3) + + var body: some View { + VStack(spacing: 100) { + OnboardingProgressIndicator(stepInfo: stepInfo) + + Button(action: { + let nextStep = stepInfo.currentStep < stepInfo.totalSteps ? stepInfo.currentStep + 1 : 1 + stepInfo = OnboardingProgressIndicator.StepInfo(currentStep: nextStep, totalSteps: stepInfo.totalSteps) + }, label: { + Text("Update Progress") + }) + } + } + } + + return PreviewWrapper() +} + +#Preview("Progress Bar") { + ProgressBarView(progress: 80) + .frame(width: 200, height: 8) +} diff --git a/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift b/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift index ea123baec5..881e5fd721 100644 --- a/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift +++ b/DuckDuckGo/OnboardingExperiment/Styles/DaxDialogStyles.swift @@ -33,16 +33,23 @@ extension OnboardingStyles { } struct BackgroundStyle: ViewModifier { + let backgroundType: OnboardingBackgroundType func body(content: Content) -> some View { ZStack { - OnboardingBackground() - .ignoresSafeArea(.keyboard) + switch backgroundType { + case let .illustratedGradient(gradientType): + OnboardingBackground() + .onboardingGradient(gradientType) + .ignoresSafeArea(.keyboard) + case let .gradientOnly(gradientType): + OnboardingGradientView(type: gradientType) + .ignoresSafeArea(.keyboard) + } content } } - } } @@ -57,8 +64,32 @@ extension View { modifier(OnboardingStyles.DaxDialogStyle()) } - func onboardingContextualBackgroundStyle() -> some View { - modifier(OnboardingStyles.BackgroundStyle()) + func onboardingContextualBackgroundStyle(background: OnboardingBackgroundType) -> some View { + modifier(OnboardingStyles.BackgroundStyle(backgroundType: background)) } } + +enum OnboardingBackgroundType { + case illustratedGradient(OnboardingGradientType) + case gradientOnly(OnboardingGradientType) +} + +enum OnboardingGradientTypeKey: EnvironmentKey { + static var defaultValue: OnboardingGradientType = .default +} + +extension EnvironmentValues { + var onboardingGradientType: OnboardingGradientType { + get { self[OnboardingGradientTypeKey.self] } + set { self[OnboardingGradientTypeKey.self] = newValue } + } +} + +extension View { + + func onboardingGradient(_ type: OnboardingGradientType) -> some View { + environment(\.onboardingGradientType, type) + } + +} diff --git a/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift b/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift index 283c8cff3a..113ea7c39e 100644 --- a/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift +++ b/DuckDuckGo/OnboardingExperiment/Styles/OnboardingTextStyles.swift @@ -30,7 +30,7 @@ extension OnboardingStyles { func body(content: Content) -> some View { let view = content .font(.system(size: fontSize, weight: .bold)) - .foregroundColor(.primary) + .foregroundStyle(Color.primary) .multilineTextAlignment(.center) if #available(iOS 16, *) { @@ -42,6 +42,22 @@ extension OnboardingStyles { } + struct ProgressBarTitleStyle: ViewModifier { + + func body(content: Content) -> some View { + let view = content + .font(.system(size: 11, weight: .bold)) + .foregroundStyle(Color.secondary) + + if #available(iOS 16, *) { + return view.kerning(0.06) + } else { + return view + } + } + + } + } extension View { @@ -49,5 +65,9 @@ extension View { func onboardingTitleStyle(fontSize: CGFloat) -> some View { modifier(OnboardingStyles.TitleStyle(fontSize: fontSize)) } - + + func onboardingProgressTitleStyle() -> some View { + modifier(OnboardingStyles.ProgressBarTitleStyle()) + } + } diff --git a/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift b/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift index 4d3c15a06c..ca4b4fc628 100644 --- a/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift +++ b/DuckDuckGo/OnboardingHelpers/OnboardingSuggestedSearchesProvider.swift @@ -22,18 +22,31 @@ import Onboarding struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding { private let countryAndLanguageProvider: OnboardingRegionAndLanguageProvider + private let onboardingManager: OnboardingHighlightsManaging - init(countryAndLanguageProvider: OnboardingRegionAndLanguageProvider = Locale.current) { + init( + countryAndLanguageProvider: OnboardingRegionAndLanguageProvider = Locale.current, + onboardingManager: OnboardingHighlightsManaging = OnboardingManager() + ) { self.countryAndLanguageProvider = countryAndLanguageProvider + self.onboardingManager = onboardingManager } var list: [ContextualOnboardingListItem] { - return [ - option1, - option2, - option3, - surpriseMe - ] + if onboardingManager.isOnboardingHighlightsEnabled { + [ + option1, + option2, + surpriseMe + ] + } else { + [ + option1, + option2, + option3, + surpriseMe + ] + } } private var country: String? { @@ -69,12 +82,14 @@ struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding } private var surpriseMe: ContextualOnboardingListItem { - var search: String - if country == "us" { - search = UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeEnglish + let search = if onboardingManager.isOnboardingHighlightsEnabled { + UserText.HighlightsOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMe } else { - search = UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeInternational + country == "us" ? + UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeEnglish : + UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeInternational } + return ContextualOnboardingListItem.surprise(title: search, visibleTitle: UserText.DaxOnboardingExperiment.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) } diff --git a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift index 0bb500669b..f16e209939 100644 --- a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift @@ -171,14 +171,14 @@ extension PrivacyDashboardViewController { private func decorate() { let theme = ThemeManager.shared.currentTheme view.backgroundColor = theme.privacyDashboardWebviewBackgroundColor - privacyDashboardController.theme = .init(theme) + privacyDashboardController.theme = .init(traitCollection) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - privacyDashboardController.theme = .init() + privacyDashboardController.theme = .init(traitCollection) } } } @@ -378,20 +378,12 @@ extension PrivacyDashboardViewController { } private extension PrivacyDashboardTheme { - init(_ userInterfaceStyle: UIUserInterfaceStyle = ThemeManager.shared.currentInterfaceStyle) { - switch userInterfaceStyle { + init(_ traitCollection: UITraitCollection) { + switch traitCollection.userInterfaceStyle { case .light: self = .light case .dark: self = .dark case .unspecified: self = .light @unknown default: self = .light } } - - init(_ theme: Theme) { - switch theme.name { - case .light: self = .light - case .dark: self = .dark - case .systemDefault: self.init(ThemeManager.shared.currentInterfaceStyle) - } - } } diff --git a/DuckDuckGo/PrivacyIconLogic.swift b/DuckDuckGo/PrivacyIconLogic.swift index 9ae915497f..727d46480e 100644 --- a/DuckDuckGo/PrivacyIconLogic.swift +++ b/DuckDuckGo/PrivacyIconLogic.swift @@ -37,8 +37,9 @@ final class PrivacyIconLogic { } else { let config = ContentBlocking.shared.privacyConfigurationManager.privacyConfig let isUserUnprotected = config.isUserUnprotected(domain: privacyInfo.url.host) - - let notFullyProtected = !privacyInfo.https || isUserUnprotected || privacyInfo.serverTrust == nil + + let isServerTrustInvalid = (privacyInfo.shouldCheckServerTrust ? privacyInfo.serverTrust == nil : false) + let notFullyProtected = !privacyInfo.https || isUserUnprotected || isServerTrustInvalid return notFullyProtected ? .shieldWithDot : .shield } diff --git a/DuckDuckGo/RootDebugViewController.swift b/DuckDuckGo/RootDebugViewController.swift index a07df2d5c0..287ef73822 100644 --- a/DuckDuckGo/RootDebugViewController.swift +++ b/DuckDuckGo/RootDebugViewController.swift @@ -91,7 +91,7 @@ class RootDebugViewController: UITableViewController { super.init(coder: coder) } - @IBSegueAction func onCreateImageCacheDebugScreen(_ coder: NSCoder, sender: Any?, segueIdentifier: String?) -> ImageCacheDebugViewController { + @IBSegueAction func onCreateImageCacheDebugScreen(_ coder: NSCoder) -> ImageCacheDebugViewController? { guard let controller = ImageCacheDebugViewController(coder: coder, bookmarksDatabase: self.bookmarksDatabase!) else { fatalError("Failed to create controller") @@ -187,7 +187,7 @@ class RootDebugViewController: UITableViewController { DuckPlayerLaunchExperiment().cleanup() ActionMessageView.present(message: "Experiment Settings deleted. You'll be assigned a random cohort") case .overrideDuckPlayerExperiment: - DuckPlayerLaunchExperiment().experimentOverride = true + DuckPlayerLaunchExperiment().override() ActionMessageView.present(message: "Overriding experiment. You are now in the 'experiment' group. Restart the app to complete") } } diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index 2428aca706..7cb7486a5e 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.136.0 + 7.137.0 Key version Title diff --git a/DuckDuckGo/SpeechRecognizer.swift b/DuckDuckGo/SpeechRecognizer.swift index 67e58d9377..119fbfedb8 100644 --- a/DuckDuckGo/SpeechRecognizer.swift +++ b/DuckDuckGo/SpeechRecognizer.swift @@ -101,7 +101,7 @@ final class SpeechRecognizer: NSObject, SpeechRecognizerProtocol { func startRecording(resultHandler: @escaping (_ text: String?, _ error: Error?, _ speechDidFinish: Bool) -> Void, - volumeCallback: @escaping(_ volume: Float) -> Void) { + volumeCallback: @escaping (_ volume: Float) -> Void) { recognitionRequest = SFSpeechAudioBufferRecognitionRequest() audioEngine = AVAudioEngine() diff --git a/DuckDuckGo/StubAutofillLoginImportStateProvider.swift b/DuckDuckGo/StubAutofillLoginImportStateProvider.swift new file mode 100644 index 0000000000..326659f898 --- /dev/null +++ b/DuckDuckGo/StubAutofillLoginImportStateProvider.swift @@ -0,0 +1,35 @@ +// +// StubAutofillLoginImportStateProvider.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 BrowserServicesKit + +struct StubAutofillLoginImportStateProvider: AutofillLoginImportStateProvider { + public var isNewDDGUser: Bool = false + public var hasImportedLogins: Bool = false + var credentialsImportPromptPresentationCount: Int = 0 + + var isAutofillEnabled: Bool { + AppDependencyProvider().appSettings.autofillCredentialsEnabled + } + + func hasNeverPromptWebsitesFor(_ domain: String) -> Bool { + AppDependencyProvider().autofillNeverPromptWebsitesManager.hasNeverPromptWebsitesFor(domain: domain) + } +} diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index f413f40730..514b8931de 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -993,7 +993,7 @@ class TabViewController: UIViewController { self.privacyInfo = privacyInfo didGoBackForward = false } else { - privacyInfo = makePrivacyInfo(url: url) + privacyInfo = makePrivacyInfo(url: url, shouldCheckServerTrust: true) } } else { privacyInfo = nil @@ -1001,14 +1001,15 @@ class TabViewController: UIViewController { onPrivacyInfoChanged() } - public func makePrivacyInfo(url: URL) -> PrivacyInfo? { + public func makePrivacyInfo(url: URL, shouldCheckServerTrust: Bool = false) -> PrivacyInfo? { guard let host = url.host else { return nil } let entity = ContentBlocking.shared.trackerDataManager.trackerData.findParentEntityOrFallback(forHost: host) let privacyInfo = PrivacyInfo(url: url, parentEntity: entity, - protectionStatus: makeProtectionStatus(for: host)) + protectionStatus: makeProtectionStatus(for: host), + shouldCheckServerTrust: shouldCheckServerTrust) let isValid = certificateTrustEvaluator.evaluateCertificateTrust(trust: webView.serverTrust) if let isValid { privacyInfo.serverTrust = isValid ? webView.serverTrust : nil diff --git a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift index 0743776c30..12ef1e53ee 100644 --- a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift @@ -372,7 +372,7 @@ extension TabViewController { private func shareLinkWithTemporaryDownload(_ temporaryDownload: Download?, originalLink: Link, - completion: @escaping(Link) -> Void) { + completion: @escaping (Link) -> Void) { guard let download = temporaryDownload else { completion(originalLink) return diff --git a/DuckDuckGo/UserScripts.swift b/DuckDuckGo/UserScripts.swift index 4b003e0a57..30904c6324 100644 --- a/DuckDuckGo/UserScripts.swift +++ b/DuckDuckGo/UserScripts.swift @@ -56,7 +56,7 @@ final class UserScripts: UserScriptsProvider { init(with sourceProvider: ScriptSourceProviding) { contentBlockerUserScript = ContentBlockerRulesUserScript(configuration: sourceProvider.contentBlockerRulesConfig) surrogatesScript = SurrogatesUserScript(configuration: sourceProvider.surrogatesConfig) - autofillUserScript = AutofillUserScript(scriptSourceProvider: sourceProvider.autofillSourceProvider) + autofillUserScript = AutofillUserScript(scriptSourceProvider: sourceProvider.autofillSourceProvider, loginImportStateProvider: StubAutofillLoginImportStateProvider()) autofillUserScript.sessionKey = sourceProvider.contentScopeProperties.sessionKey loginFormDetectionScript = sourceProvider.loginDetectionEnabled ? LoginFormDetectionUserScript() : nil diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 179a31c84e..8410fd0612 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1327,9 +1327,9 @@ But if you *do* want a peek under the hood, you can find more information about enum Features { public static let privateSearch = NSLocalizedString("onboarding.browsers.features.privateSearch.title", value: "Search privately by default", comment: "Message to highlight browser capability of private searches") public static let trackerBlockers = NSLocalizedString("onboarding.browsers.features.trackerBlocker.title", value: "Block 3rd-party trackers", comment: "Message to highlight browser capability ofblocking 3rd party trackers") - public static let cookiePopups = NSLocalizedString("onboarding.browsers.features.cookiePopups.title", value: "Block cookie pop-ups", comment: "Message to highlight browser capability of blocking cookie pop-ups") + public static let cookiePopups = NSLocalizedString("onboarding.browsers.features.cookiePopups.title", value: "Block cookie pop-ups", comment: "Message to highlight how the browser allows you to block cookie pop-ups") public static let creepyAds = NSLocalizedString("onboarding.browsers.features.creepyAds.title", value: "Block creepy ads", comment: "Message to highlight browser capability of blocking creepy ads") - public static let eraseBrowsingData = NSLocalizedString("onboarding.browsers.features.eraseBrowsingData.title", value: "Swiftly erase browsing data", comment: "Message to highlight browser capability ofswiftly erase browsing data") + public static let eraseBrowsingData = NSLocalizedString("onboarding.browsers.features.eraseBrowsingData.title", value: "Swiftly erase browsing data", comment: "Message to highlight browser capability of swiftly erase browsing data") } } @@ -1358,4 +1358,48 @@ But if you *do* want a peek under the hood, you can find more information about static let daxDialogBrowsingWithMultipleTrackers = NSLocalizedString("contextual.onboarding.browsing.multiple.trackers", comment: "First parameter is a count of additional trackers, second and third are names of the tracker networks (strings)") } } + + public enum HighlightsOnboardingExperiment { + enum Intro { + public static let title = NSLocalizedString("onboarding.highlights.intro.title", value: "Hi there.\n\nReady for a faster browser that keeps you protected?", comment: "The title of the onboarding dialog popup") + } + + enum BrowsersComparison { + public static let title = NSLocalizedString("onboarding.highlights.browsers.title", value: "Protections activated!", comment: "The title of the dialog to show the privacy features that DuckDuckGo offers") + + enum Features { + public static let trackerBlockers = NSLocalizedString("onboarding.highlights.browsers.features.trackerBlocker.title", value: "Block 3rd party trackers", comment: "Message to highlight browser capability ofblocking 3rd party trackers") + public static let cookiePopups = NSLocalizedString("onboarding.highlights.browsers.features.cookiePopups.title", value: "Block cookie requests & popups", comment: "Message to highlight how the browser allows you to block cookie pop-ups") + public static let creepyAds = NSLocalizedString("onboarding.highlights.browsers.features.creepyAds.title", value: "Block targeted ads", comment: "Message to highlight browser capability of blocking creepy ads") + public static let eraseBrowsingData = NSLocalizedString("onboarding.highlights.browsers.features.eraseBrowsingData.title", value: "Erase browsing data swiftly", comment: "Message to highlight browser capability of swiftly erase browsing data") + } + } + + enum AppIconSelection { + public static let title = NSLocalizedString("onboarding.highlights.appIconSelection.title", value: "Which color looks best on me?", comment: "The title of the onboarding dialog popup to select the preferred App icon.") + public static let message = NSLocalizedString("onboarding.highlights.appIconSelection.message", value: "Pick your app icon:", comment: "The subheader of the onboarding dialog popup to select the preferred App icon.") + public static let cta = NSLocalizedString("onboarding.highlights.appIconSelection.cta", value: "Next", comment: "The title of the CTA to progress to the next onboarding screen.") + } + + enum AddressBarPosition { + public static let title = NSLocalizedString("onboarding.highlights.addressBarPosition.title", value: "Where should I put your address bar?", comment: "The title of the onboarding dialog popup to select the preferred address bar position.") + public static let topTitle = NSLocalizedString("onboarding.highlights.addressBarPosition.top.title", value: "Top", comment: "The title of the option to set the address bar to the top.") + public static let defaultOption = NSLocalizedString("onboarding.highlights.addressBarPosition.default", value: "(Default)", comment: "Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default)") + public static let topMessage = NSLocalizedString("onboarding.highlights.addressBarPosition.top.message", value: "Easy to see", comment: "The message of the option to set the address bar to the top.") + public static let bottomTitle = NSLocalizedString("onboarding.highlights.addressBarPosition.bottom.title", value: "Bottom", comment: "The title of the option to set the address bar to the bottom.") + public static let bottomMessage = NSLocalizedString("onboarding.highlights.addressBarPosition.bottom.message", value: "Easy to reach", comment: "The message of the option to set the address bar to the bottom.") + public static let cta = NSLocalizedString("onboarding.highlights.addressBarPosition.cta", value: "Next", comment: "The title of the CTA to progress to the next onboarding screen.") + } + + enum ContextualOnboarding { + static let onboardingTryASearchMessage = NSLocalizedString("contextual.onboarding.highlights.try-a-search.message", value: "Your DuckDuckGo searches are always private.", comment: "Message of a popover on the browser that invites the user to try a search explaining that their searches are private") + static let onboardingFirstSearchDoneMessage = NSLocalizedString("contextual.onboarding.highlights.first-search-done.message", value: "That’s DuckDuckGo Search! Private. Fast. Fewer ads.", comment: "After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo") + static let onboardingFinalScreenMessage = NSLocalizedString("contextual.onboarding.highlights.final-screen.message", value: "Remember: every time you browse with me a creepy ad loses its wings.", comment: "Message of the last screen of the onboarding to the browser app.") + static let tryASearchOptionSurpriseMe = NSLocalizedString("contextual.onboarding.highlights.try-search.surprise-me", value: "baby ducklings", comment: "Browser Search query for baby ducklings") + } + + enum FireDialog { + public static let skip = NSLocalizedString("onboarding.highlights.fireDialog.cta.skip", value: "Skip", comment: "The title of the fire button CTA to skip erasing the data.") + } + } } diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index a68ea3a7d8..ec91ba69c6 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -770,6 +770,18 @@ /* During onboarding steps this button is shown and takes either to the next steps or closes the onboarding. */ "contextual.onboarding.got-it.button" = "Got it!"; +/* Message of the last screen of the onboarding to the browser app. */ +"contextual.onboarding.highlights.final-screen.message" = "Remember: every time you browse with me a creepy ad loses its wings."; + +/* After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo */ +"contextual.onboarding.highlights.first-search-done.message" = "That’s DuckDuckGo Search! Private. Fast. Fewer ads."; + +/* Message of a popover on the browser that invites the user to try a search explaining that their searches are private */ +"contextual.onboarding.highlights.try-a-search.message" = "Your DuckDuckGo searches are always private."; + +/* Browser Search query for baby ducklings */ +"contextual.onboarding.highlights.try-search.surprise-me" = "baby ducklings"; + /* Title of a popover on the new tab page browser that invites the user to try a visiting a website */ "contextual.onboarding.ntp.try-a-site.title" = "Try visiting a site!"; @@ -1811,13 +1823,13 @@ https://duckduckgo.com/mac"; /* Button to change the default browser */ "onboarding.browsers.cta" = "Choose Your Browser"; -/* Message to highlight browser capability of blocking cookie pop-ups */ +/* Message to highlight how the browser allows you to block cookie pop-ups */ "onboarding.browsers.features.cookiePopups.title" = "Block cookie pop-ups"; /* Message to highlight browser capability of blocking creepy ads */ "onboarding.browsers.features.creepyAds.title" = "Block creepy ads"; -/* Message to highlight browser capability ofswiftly erase browsing data */ +/* Message to highlight browser capability of swiftly erase browsing data */ "onboarding.browsers.features.eraseBrowsingData.title" = "Swiftly erase browsing data"; /* Message to highlight browser capability of private searches */ @@ -1829,6 +1841,57 @@ https://duckduckgo.com/mac"; /* The title of the dialog to show the privacy features that DuckDuckGo offers */ "onboarding.browsers.title" = "Privacy protections activated!"; +/* The message of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.message" = "Easy to reach"; + +/* The title of the option to set the address bar to the bottom. */ +"onboarding.highlights.addressBarPosition.bottom.title" = "Bottom"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.addressBarPosition.cta" = "Next"; + +/* Indicates what address bar option (Top/Bottom) is the default one. E.g. Top (Default) */ +"onboarding.highlights.addressBarPosition.default" = "(Default)"; + +/* The title of the onboarding dialog popup to select the preferred address bar position. */ +"onboarding.highlights.addressBarPosition.title" = "Where should I put your address bar?"; + +/* The message of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.message" = "Easy to see"; + +/* The title of the option to set the address bar to the top. */ +"onboarding.highlights.addressBarPosition.top.title" = "Top"; + +/* The title of the CTA to progress to the next onboarding screen. */ +"onboarding.highlights.appIconSelection.cta" = "Next"; + +/* The subheader of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.message" = "Pick your app icon:"; + +/* The title of the onboarding dialog popup to select the preferred App icon. */ +"onboarding.highlights.appIconSelection.title" = "Which color looks best on me?"; + +/* Message to highlight how the browser allows you to block cookie pop-ups */ +"onboarding.highlights.browsers.features.cookiePopups.title" = "Block cookie requests & popups"; + +/* Message to highlight browser capability of blocking creepy ads */ +"onboarding.highlights.browsers.features.creepyAds.title" = "Block targeted ads"; + +/* Message to highlight browser capability of swiftly erase browsing data */ +"onboarding.highlights.browsers.features.eraseBrowsingData.title" = "Erase browsing data swiftly"; + +/* Message to highlight browser capability ofblocking 3rd party trackers */ +"onboarding.highlights.browsers.features.trackerBlocker.title" = "Block 3rd party trackers"; + +/* The title of the dialog to show the privacy features that DuckDuckGo offers */ +"onboarding.highlights.browsers.title" = "Protections activated!"; + +/* The title of the fire button CTA to skip erasing the data. */ +"onboarding.highlights.fireDialog.cta.skip" = "Skip"; + +/* The title of the onboarding dialog popup */ +"onboarding.highlights.intro.title" = "Hi there.\n\nReady for a faster browser that keeps you protected?"; + /* Button to continue the onboarding process */ "onboarding.intro.cta" = "Let’s do it!"; diff --git a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift index 1c6a2a6d7c..eb21f4507b 100644 --- a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift +++ b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift @@ -124,16 +124,13 @@ final class AdAttributionPixelReporterTests: XCTestCase { XCTAssertNil(pixelAttributes["ad_id"]) } - func testNotAttributedPixelFiredAndMarkedReported_WhenAttributionFalse() async throws { + func testPixelNotFiredAndMarksReport_WhenAttributionFalse() async { let sut = createSUT() attributionFetcher.fetchResponse = ("example", AdServicesAttributionResponse(attribution: false)) let result = await sut.reportAttributionIfNeeded() - let pixelAttributes = try XCTUnwrap(PixelFiringMock.lastParams) - - XCTAssertEqual(pixelAttributes, [:]) - XCTAssertEqual(PixelFiringMock.lastPixel?.name, "m_apple-ad-attribution_not-attributed") + XCTAssertNil(PixelFiringMock.lastPixel) XCTAssertTrue(fetcherStorage.wasAttributionReportSuccessful) XCTAssertTrue(result) } diff --git a/DuckDuckGoTests/AppIconPickerViewModelTests.swift b/DuckDuckGoTests/AppIconPickerViewModelTests.swift new file mode 100644 index 0000000000..76088f3ded --- /dev/null +++ b/DuckDuckGoTests/AppIconPickerViewModelTests.swift @@ -0,0 +1,125 @@ +// +// AppIconPickerViewModelTests.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 +@testable import DuckDuckGo + +final class AppIconPickerViewModelTests: XCTestCase { + private var sut: AppIconPickerViewModel! + private var appIconManagerMock: AppIconManagerMock! + + @MainActor + override func setUpWithError() throws { + try super.setUpWithError() + + appIconManagerMock = AppIconManagerMock() + sut = AppIconPickerViewModel(appIconManager: appIconManagerMock) + } + + override func tearDownWithError() throws { + appIconManagerMock = nil + sut = nil + try super.tearDownWithError() + } + + @MainActor + func testWhenItemsIsCalledThenIconsAreReturned() { + // GIVEN + let expectedIcons: [AppIcon] = [.red, .yellow, .green, .blue, .purple, .black] + + // WHEN + let result = sut.items + + // THEN + XCTAssertEqual(result.map(\.icon), expectedIcons) + } + + @MainActor + func testWhenInitThenSelectedAppIconIsReturned() { + // GIVEN + appIconManagerMock.appIcon = .purple + sut = AppIconPickerViewModel(appIconManager: appIconManagerMock) + + // WHEN + let result = sut.items + + // THEN + XCTAssertEqual(result.count, AppIcon.allCases.count) + assertSelected(.purple, items: result) + } + + @MainActor + func testWhenChangeAppIconIsCalledAndManagerFailsThenSelectedAppIconIsNotUpdated() { + // GIVEN + appIconManagerMock.appIcon = .red + appIconManagerMock.changeAppIconError = NSError(domain: #function, code: 0) + assertSelected(.red, items: sut.items) + + // WHEN + sut.changeApp(icon: .purple) + + // THEN + assertSelected(.red, items: sut.items) + } + + @MainActor + func testWhenChangeAppIconIsCalledThenShouldAskAppIconManagerToChangeAppIcon() { + // GIVEN + XCTAssertFalse(appIconManagerMock.didCallChangeAppIcon) + XCTAssertNil(appIconManagerMock.capturedAppIcon) + + // WHEN + sut.changeApp(icon: .purple) + + // THEN + XCTAssertTrue(appIconManagerMock.didCallChangeAppIcon) + XCTAssertEqual(appIconManagerMock.capturedAppIcon, .purple) + } + + private func assertSelected(_ appIcon: AppIcon, items: [AppIconPickerViewModel.DisplayModel]) { + items.forEach { model in + if model.icon == appIcon { + XCTAssertTrue(model.isSelected) + } else { + XCTAssertFalse(model.isSelected) + } + } + } +} + +final class AppIconManagerMock: AppIconManaging { + private(set) var didCallChangeAppIcon = false + private(set) var capturedAppIcon: AppIcon? + + var appIcon: DuckDuckGo.AppIcon = .red + + var changeAppIconError: Error? + + func changeAppIcon(_ appIcon: AppIcon, completionHandler: (((any Error)?) -> Void)?) { + didCallChangeAppIcon = true + capturedAppIcon = appIcon + + if let changeAppIconError { + completionHandler?(changeAppIconError) + } else { + completionHandler?(nil) + } + } + +} diff --git a/DuckDuckGoTests/AutofillHeaderViewFactoryTests.swift b/DuckDuckGoTests/AutofillHeaderViewFactoryTests.swift new file mode 100644 index 0000000000..b2f67e28b8 --- /dev/null +++ b/DuckDuckGoTests/AutofillHeaderViewFactoryTests.swift @@ -0,0 +1,144 @@ +// +// AutofillHeaderViewFactoryTests.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 SwiftUI +@testable import DuckDuckGo + +class MockAutofillHeaderViewDelegate: AutofillHeaderViewDelegate { + var didHandlePrimaryAction = false + var didHandleDismissAction = false + var lastHandledHeaderType: AutofillHeaderViewFactory.ViewType? + + func handlePrimaryAction(for headerType: AutofillHeaderViewFactory.ViewType) { + didHandlePrimaryAction = true + lastHandledHeaderType = headerType + } + + func handleDismissAction(for headerType: AutofillHeaderViewFactory.ViewType) { + didHandleDismissAction = true + lastHandledHeaderType = headerType + } +} + +final class AutofillHeaderViewFactoryTests: XCTestCase { + + var factory: AutofillHeaderViewFactory! + var mockDelegate: MockAutofillHeaderViewDelegate! + + override func setUpWithError() throws { + try super.setUpWithError() + + mockDelegate = MockAutofillHeaderViewDelegate() + factory = AutofillHeaderViewFactory(delegate: mockDelegate) + } + + override func tearDownWithError() throws { + factory = nil + mockDelegate = nil + + try super.tearDownWithError() + } + + func testWhenMakeHeaderViewForSyncPromoThenSyncPromoViewIsReturned() { + let viewController = factory.makeHeaderView(for: .syncPromo(.passwords)) + XCTAssertTrue(viewController is UIHostingController) + if let hostingController = viewController as? UIHostingController { + XCTAssertNotNil(hostingController.rootView) + } + } + + func testWhenMakeHeaderViewForSurveyThenAutofillSurveyViewIsReturned() { + let survey = AutofillSurveyManager.AutofillSurvey(id: "testSurvey", url: "https://example.com") + let viewController = factory.makeHeaderView(for: .survey(survey)) + + XCTAssertTrue(viewController is UIHostingController) + if let hostingController = viewController as? UIHostingController { + XCTAssertNotNil(hostingController.rootView) + } + } + + func testWhenSyncPromoPrimaryButtonActionIsCalledThenDelegateHandlePrimaryActionIsCalled() throws { + let touchpoint = SyncPromoManager.Touchpoint.passwords + let viewController = try XCTUnwrap(factory.makeHeaderView(for: .syncPromo(touchpoint)) as? UIHostingController, "Expected a UIHostingController") + + let primaryButtonAction = try XCTUnwrap(viewController.rootView.viewModel.primaryButtonAction, "Primary button action should not be nil") + primaryButtonAction() + + XCTAssertTrue(mockDelegate.didHandlePrimaryAction) + if case .syncPromo(let receivedTouchpoint) = mockDelegate.lastHandledHeaderType { + XCTAssertEqual(receivedTouchpoint, touchpoint) + } else { + XCTFail("Expected .syncPromo ViewType with touchpoint \(touchpoint)") + } + } + + func testWhenSyncPromoDismissButtonActionIsCalledThenDelegateHandleDismissActionIsCalled() throws { + let touchpoint = SyncPromoManager.Touchpoint.passwords + + let viewController = try XCTUnwrap(factory.makeHeaderView(for: .syncPromo(touchpoint)) as? UIHostingController, "Expected a UIHostingController") + + let dismissButtonAction = try XCTUnwrap(viewController.rootView.viewModel.dismissButtonAction, "Dismiss button action should not be nil") + dismissButtonAction() + + XCTAssertTrue(mockDelegate.didHandleDismissAction) + + if case .syncPromo(let receivedTouchpoint) = mockDelegate.lastHandledHeaderType { + XCTAssertEqual(receivedTouchpoint, touchpoint) + } else { + XCTFail("Expected .syncPromo ViewType with touchpoint \(touchpoint)") + } + } + + func testWhenSurveyPrimaryButtonActionIsCalledThenDelegateHandlePrimaryActionIsCalled() throws { + let survey = AutofillSurveyManager.AutofillSurvey(id: "testSurvey", url: "https://example.com") + + let viewController = try XCTUnwrap(factory.makeHeaderView(for: .survey(survey)) as? UIHostingController, "Expected a UIHostingController") + + let primaryButtonAction = try XCTUnwrap(viewController.rootView.primaryButtonAction, "Primary button action should not be nil") + primaryButtonAction() + + XCTAssertTrue(mockDelegate.didHandlePrimaryAction) + + if case .survey(let receivedSurvey) = mockDelegate.lastHandledHeaderType { + XCTAssertEqual(receivedSurvey.id, survey.id) + XCTAssertEqual(receivedSurvey.url, survey.url) + } else { + XCTFail("Expected .survey ViewType with survey \(survey)") + } + } + + func testWhenSurveyDismissButtonActionIsCalledThenDelegateHandleDismissActionIsCalled() throws { + let survey = AutofillSurveyManager.AutofillSurvey(id: "testSurvey", url: "https://example.com") + + let viewController = try XCTUnwrap(factory.makeHeaderView(for: .survey(survey)) as? UIHostingController, "Expected a UIHostingController") + + let dismissButtonAction = try XCTUnwrap(viewController.rootView.dismissButtonAction, "Dismiss button action should not be nil") + dismissButtonAction() + + XCTAssertTrue(mockDelegate.didHandleDismissAction) + + if case .survey(let receivedSurvey) = mockDelegate.lastHandledHeaderType { + XCTAssertEqual(receivedSurvey.id, survey.id) + XCTAssertEqual(receivedSurvey.url, survey.url) + } else { + XCTFail("Expected .survey ViewType with survey \(survey)") + } + } +} diff --git a/DuckDuckGoTests/AutofillLoginListViewModelTests.swift b/DuckDuckGoTests/AutofillLoginListViewModelTests.swift index f4f0880d8e..0ff5c5ac3a 100644 --- a/DuckDuckGoTests/AutofillLoginListViewModelTests.swift +++ b/DuckDuckGoTests/AutofillLoginListViewModelTests.swift @@ -50,6 +50,17 @@ class AutofillLoginListViewModelTests: XCTestCase { } ] }, + "autofillSurveys": { + "state": "enabled", + "settings": { + "surveys": [ + { + "id": "123", + "url": "https://asurveyurl.com" + } + ] + }, + }, }, "unprotectedTemporary": [] } @@ -65,6 +76,17 @@ class AutofillLoginListViewModelTests: XCTestCase { }, "exceptions": [] }, + "autofillSurveys": { + "state": "disabled", + "settings": { + "surveys": [ + { + "id": "240900", + "url": "https://asurveyurl.com" + } + ] + }, + }, }, "unprotectedTemporary": [] } @@ -72,6 +94,7 @@ class AutofillLoginListViewModelTests: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() + setupUserDefault(with: #file) manager = AutofillNeverPromptWebsitesManager(secureVault: vault) syncService = MockDDGSyncing(authState: .inactive, scheduler: CapturingScheduler(), isSyncInProgress: false) } @@ -492,7 +515,7 @@ class AutofillLoginListViewModelTests: XCTestCase { let model = AutofillLoginListViewModel(appSettings: appSettings, tld: tld, secureVault: vault, - currentTabUrl: URL(string: "https://\(testDomain)"), + currentTabUrl: currentTabUrl, currentTabUid: "1", autofillNeverPromptWebsitesManager: manager, privacyConfig: makePrivacyConfig(from: configEnabled), @@ -529,6 +552,60 @@ class AutofillLoginListViewModelTests: XCTestCase { XCTAssertTrue(model.shouldShowBreakageReporter()) } + + func testWhenLocaleIsNotEnglishThenNoSurveyIsReturned() { + let nonEnglishLocale = Locale(identifier: "es") + let model = AutofillLoginListViewModel(appSettings: appSettings, tld: tld, secureVault: vault, autofillNeverPromptWebsitesManager: manager, syncService: syncService, locale: nonEnglishLocale) + + XCTAssertNil(model.getSurveyToPresent()) + } + + func testWhenViewStateIsIneligibleThenNoSurveyIsReturned() throws { + vault.storedAccounts = [ + SecureVaultModels.WebsiteAccount(id: "1", title: nil, username: "", domain: "testsite.com", created: Date(), lastUpdated: Date()), + SecureVaultModels.WebsiteAccount(id: "2", title: nil, username: "", domain: "testsite.com", created: Date(), lastUpdated: Date()) + ] + for account in vault.storedAccounts { + _ = try vault.storeWebsiteCredentials(SecureVaultModels.WebsiteCredentials(account: account, password: nil)) + } + let model = AutofillLoginListViewModel(appSettings: appSettings, tld: tld, secureVault: vault, autofillNeverPromptWebsitesManager: manager, syncService: syncService) + + XCTAssertNil(model.getSurveyToPresent()) + } + + func testWhenIsEditingThenNoSurveyIsReturned() { + let model = AutofillLoginListViewModel(appSettings: appSettings, tld: tld, secureVault: vault, autofillNeverPromptWebsitesManager: manager, syncService: syncService) + model.isEditing = true + + XCTAssertNil(model.getSurveyToPresent()) + } + + func testWhenSurveyConfigIsDisabledThenNoSurveyIsReturned() { + let model = AutofillLoginListViewModel(appSettings: appSettings, + tld: tld, + secureVault: vault, + privacyConfig: makePrivacyConfig(from: configDisabled), + syncService: syncService) + + XCTAssertNil(model.getSurveyToPresent()) + } + + func testWhenAllConditionsAreMetThenSurveyIsReturnedAndWhenDismissedNotSurveyIsReturned() { + let model = AutofillLoginListViewModel(appSettings: appSettings, + tld: tld, + secureVault: vault, + privacyConfig: makePrivacyConfig(from: configEnabled), + syncService: syncService) + let survey = model.getSurveyToPresent() + XCTAssertNotNil(survey) + XCTAssertEqual(survey?.id, "123") + XCTAssertEqual(survey?.url, "https://asurveyurl.com") + + model.dismissSurvey(id: "123") + + XCTAssertNil(model.getSurveyToPresent()) + } + } class AutofillLoginListSectionTypeTests: XCTestCase { diff --git a/DuckDuckGoTests/AutofillSurveyManagerTests.swift b/DuckDuckGoTests/AutofillSurveyManagerTests.swift new file mode 100644 index 0000000000..61f8e350b7 --- /dev/null +++ b/DuckDuckGoTests/AutofillSurveyManagerTests.swift @@ -0,0 +1,118 @@ +// +// AutofillSurveyManagerTests.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 BrowserServicesKit +@testable import DuckDuckGo + +final class AutofillSurveyManagerTests: XCTestCase { + + private var manager: AutofillSurveyManager! + + override func setUpWithError() throws { + try super.setUpWithError() + + setupUserDefault(with: #file) + manager = AutofillSurveyManager() + manager.resetSurveys() + } + + override func tearDownWithError() throws { + manager.resetSurveys() + manager = nil + + try super.tearDownWithError() + } + + func testSurveyToPresentReturnsCorrectSurvey() { + let settings: [String: Any] = [ + "surveys": [ + ["id": "1", "url": "https://example.com/survey1"], + ["id": "2", "url": "https://example.com/survey2"] + ] + ] + + let survey = manager.surveyToPresent(settings: settings as PrivacyConfigurationData.PrivacyFeature.FeatureSettings) + XCTAssertNotNil(survey) + XCTAssertEqual(survey?.id, "1") + XCTAssertEqual(survey?.url, "https://example.com/survey1") + } + + func testSurveyToPresentSkipsCompletedSurveys() { + let settings: [String: Any] = [ + "surveys": [ + ["id": "1", "url": "https://example.com/survey1"], + ["id": "2", "url": "https://example.com/survey2"] + ] + ] + + manager.markSurveyAsCompleted(id: "1") + + let survey = manager.surveyToPresent(settings: settings as PrivacyConfigurationData.PrivacyFeature.FeatureSettings) + XCTAssertNotNil(survey) + XCTAssertEqual(survey?.id, "2") + XCTAssertEqual(survey?.url, "https://example.com/survey2") + } + + func testBuildSurveyUrlValid() { + let url = "https://example.com/survey" + let accountsCount = 5 + let resultUrl = manager.buildSurveyUrl(url, accountsCount: accountsCount) + XCTAssertNotNil(resultUrl) + XCTAssertEqual(resultUrl?.host, "example.com") + XCTAssertTrue(resultUrl?.query?.contains("saved_passwords=some") ?? false) + } + + func testAddPasswordsCountSurveyParameter() { + let baseURL = URL(string: "https://example.com/survey")! + let modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 10) + XCTAssertNotNil(modifiedURL) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=some"), true) + } + + func testPasswordsCountHasCorrectBucketNameSurveyParameter() { + let baseURL = URL(string: "https://example.com/survey")! + var modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 0) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=none"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 1) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=few"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 3) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=few"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 4) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=some"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 10) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=some"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 11) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=many"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 49) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=many"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 50) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=lots"), true) + + modifiedURL = manager.buildSurveyUrl(baseURL.absoluteString, accountsCount: 100) + XCTAssertEqual(modifiedURL?.query?.contains("saved_passwords=lots"), true) + } +} diff --git a/DuckDuckGoTests/BrowserComparisonModelTests.swift b/DuckDuckGoTests/BrowserComparisonModelTests.swift new file mode 100644 index 0000000000..846c986f20 --- /dev/null +++ b/DuckDuckGoTests/BrowserComparisonModelTests.swift @@ -0,0 +1,146 @@ +// +// BrowserComparisonModelTests.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 +@testable import DuckDuckGo + +final class BrowserComparisonModelTests: XCTestCase { + private var onboardingManager: OnboardingManagerMock! + + override func setUpWithError() throws { + try super.setUpWithError() + onboardingManager = OnboardingManagerMock() + } + + override func tearDownWithError() throws { + onboardingManager = nil + try super.tearDownWithError() + } + + func testWhenIsNotHighlightsThenBrowserComparisonFeaturePrivateSearchIsCorrect() throws { + // GIVEN + try [false, true].forEach { isOnboardingHighlightsEnabled in + onboardingManager.isOnboardingHighlightsEnabled = isOnboardingHighlightsEnabled + BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + + // WHEN + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .privateSearch })?.type.title) + + // THEN + XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.Features.privateSearch) + } + } + + func testWhenIsNotHighlightsThenBrowserComparisonFeatureBlockThirdPartyTrackersIsCorrect() throws { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = false + BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + + // WHEN + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockThirdPartyTrackers })?.type.title) + + // THEN + XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.Features.trackerBlockers) + } + + func testWhenIsHighlightsThenBrowserComparisonFeatureBlockThirdPartyTrackersIsCorrect() throws { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + + // WHEN + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockThirdPartyTrackers })?.type.title) + + // THEN + XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.trackerBlockers) + } + + func testWhenIsNotHighlightsThenBrowserComparisonFeatureBlockCookiePopupsIsCorrect() throws { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = false + BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + + // WHEN + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockCookiePopups })?.type.title) + + // THEN + XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.Features.cookiePopups) + } + + func testWhenIsHighlightsThenBrowserComparisonFeatureBlockCookiePopupsIsCorrect() throws { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + + // WHEN + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockCookiePopups })?.type.title) + + // THEN + XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.cookiePopups) + } + + func testWhenIsNotHighlightsThenBrowserComparisonFeatureBlockCreepyAdsIsCorrect() throws { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = false + BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + + // WHEN + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockCreepyAds })?.type.title) + + // THEN + XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.Features.creepyAds) + } + + func testWhenIsHighlightsThenBrowserComparisonFeatureBlockCreepyAdsIsCorrect() throws { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + + // WHEN + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .blockCreepyAds })?.type.title) + + // THEN + XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.creepyAds) + } + + func testWhenIsNotHighlightsThenBrowserComparisonFeatureEraseBrowsingDataIsCorrect() throws { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = false + BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + + // WHEN + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .eraseBrowsingData })?.type.title) + + // THEN + XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData) + } + + func testWhenIsHighlightsThenBrowserComparisonFeatureEraseBrowsingDataIsCorrect() throws { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + BrowsersComparisonModel.PrivacyFeature.FeatureType.onboardingManager = onboardingManager + + // WHEN + let result = try XCTUnwrap(BrowsersComparisonModel.privacyFeatures.first(where: { $0.type == .eraseBrowsingData })?.type.title) + + // THEN + XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.Features.eraseBrowsingData) + } + +} diff --git a/DuckDuckGoTests/DataStoreIdManagerTests.swift b/DuckDuckGoTests/DataStoreIdManagerTests.swift new file mode 100644 index 0000000000..f4d1e3d2df --- /dev/null +++ b/DuckDuckGoTests/DataStoreIdManagerTests.swift @@ -0,0 +1,70 @@ +// +// DataStoreIdManagerTests.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 +@testable import Core +import WebKit +import TestUtils + +class DataStoreIdManagerTests: XCTestCase { + + func testWhenFreshlyInstalledThenIdIsAllocated() { + let manager = DataStoreIdManager(store: MockKeyValueStore()) + + XCTAssertNil(manager.currentId) + manager.invalidateCurrentIdAndAllocateNew() + XCTAssertNotNil(manager.currentId) + } + + func testWhenIdIsInvalidatedThenNewIsCreated() { + + let manager = DataStoreIdManager(store: MockKeyValueStore()) + + XCTAssertNil(manager.currentId) + manager.invalidateCurrentIdAndAllocateNew() + + let firstID = manager.currentId + XCTAssertNotNil(firstID) + + manager.invalidateCurrentIdAndAllocateNew() + let secondID = manager.currentId + + XCTAssertNotEqual(firstID, secondID) + + manager.invalidateCurrentIdAndAllocateNew() + let thirdID = manager.currentId + + XCTAssertNotEqual(firstID, thirdID) + XCTAssertNotEqual(secondID, thirdID) + } + + func testWhenIdAlreadyExistsThenItIsRedFromTheStore() { + + let storedUUID = UUID().uuidString + let store = MockKeyValueStore() + store.set(storedUUID, forKey: DataStoreIdManager.Constants.currentWebContainerId.rawValue) + + let manager = DataStoreIdManager(store: store) + + XCTAssertEqual(manager.currentId?.uuidString, storedUUID) + } + +} diff --git a/DuckDuckGoTests/FireButtonReferenceTests.swift b/DuckDuckGoTests/FireButtonReferenceTests.swift index 95fe1ea718..2a92662ec5 100644 --- a/DuckDuckGoTests/FireButtonReferenceTests.swift +++ b/DuckDuckGoTests/FireButtonReferenceTests.swift @@ -21,6 +21,7 @@ import XCTest import os.log import WebKit @testable import Core +import TestUtils final class FireButtonReferenceTests: XCTestCase { @@ -74,7 +75,7 @@ final class FireButtonReferenceTests: XCTestCase { // Pretend the webview was loaded and the cookies were previously consumed cookieStorage.isConsumed = true - await WebCacheManager.shared.clear(cookieStorage: cookieStorage, logins: preservedLogins, dataStoreIdManager: NullDataStoreIdManager()) + await WebCacheManager.shared.clear(cookieStorage: cookieStorage, logins: preservedLogins, dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) let testCookie = cookieStorage.cookies.filter { $0.name == test.cookieName }.first @@ -157,11 +158,3 @@ private struct Test: Codable { let expectCookieRemoved: Bool let exceptPlatforms: [String] } - -private struct NullDataStoreIdManager: DataStoreIdManaging { - - var id: UUID? { return nil } - var hasId: Bool { return false } - func allocateNewContainerId() { } - -} diff --git a/DuckDuckGoTests/MarketplaceAdPostbackManagerTests.swift b/DuckDuckGoTests/MarketplaceAdPostbackManagerTests.swift new file mode 100644 index 0000000000..332aa04d7b --- /dev/null +++ b/DuckDuckGoTests/MarketplaceAdPostbackManagerTests.swift @@ -0,0 +1,99 @@ +// +// MarketplaceAdPostbackManagerTests.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 +@testable import Core +import Foundation + +class MarketplaceAdPostbackManagerTests: XCTestCase { + func testSendAppLaunchPostback_NewUser() { + let mockReturnUserMeasurement = MockReturnUserMeasurement(isReturningUser: false) + let mockUpdater = MockMarketplaceAdPostbackUpdater() + let mockStorage = MockMarketplaceAdPostbackStorage(isReturningUser: false) + let manager = MarketplaceAdPostbackManager(storage: mockStorage, updater: mockUpdater, returningUserMeasurement: mockReturnUserMeasurement) + + manager.sendAppLaunchPostback() + + XCTAssertEqual(mockUpdater.postbackSent, .installNewUser) + XCTAssertEqual(mockUpdater.postbackSent?.coarseValue, .high) + XCTAssertEqual(mockUpdater.lockPostbackSent, true) + } + + func testSendAppLaunchPostback_ReturningUser() { + let mockReturnUserMeasurement = MockReturnUserMeasurement(isReturningUser: true) + let mockUpdater = MockMarketplaceAdPostbackUpdater() + let mockStorage = MockMarketplaceAdPostbackStorage(isReturningUser: true) + let manager = MarketplaceAdPostbackManager(storage: mockStorage, updater: mockUpdater, returningUserMeasurement: mockReturnUserMeasurement) + + manager.sendAppLaunchPostback() + + XCTAssertEqual(mockUpdater.postbackSent, .installReturningUser) + XCTAssertEqual(mockUpdater.postbackSent?.coarseValue, .low) + XCTAssertEqual(mockUpdater.lockPostbackSent, true) + } + + func testSendAppLaunchPostback_AfterMeasurementChangesState() { + /// Sets return user to true to mock the situation where the user is opening the app again + /// If the storage is set to false, it should still be set as new user + let mockReturnUserMeasurement = MockReturnUserMeasurement(isReturningUser: true) + let mockUpdater = MockMarketplaceAdPostbackUpdater() + let mockStorage = MockMarketplaceAdPostbackStorage(isReturningUser: false) + let manager = MarketplaceAdPostbackManager(storage: mockStorage, updater: mockUpdater, returningUserMeasurement: mockReturnUserMeasurement) + + manager.sendAppLaunchPostback() + + XCTAssertEqual(mockUpdater.postbackSent, .installNewUser) + XCTAssertEqual(mockUpdater.postbackSent?.coarseValue, .high) + XCTAssertEqual(mockUpdater.lockPostbackSent, true) + } +} + +private final class MockReturnUserMeasurement: ReturnUserMeasurement { + func installCompletedWithATB(_ atb: Core.Atb) { } + + func updateStoredATB(_ atb: Core.Atb) { } + + var isReturningUser: Bool + + init(isReturningUser: Bool) { + self.isReturningUser = isReturningUser + } +} + +private final class MockMarketplaceAdPostbackUpdater: MarketplaceAdPostbackUpdating { + var postbackSent: MarketplaceAdPostback? + var lockPostbackSent: Bool? + + func updatePostback(_ postback: MarketplaceAdPostback, lockPostback: Bool) { + postbackSent = postback + lockPostbackSent = lockPostback + } +} + +private final class MockMarketplaceAdPostbackStorage: MarketplaceAdPostbackStorage { + var isReturningUser: Bool? + + init(isReturningUser: Bool?) { + self.isReturningUser = isReturningUser + } + + func updateReturningUserValue(_ value: Bool) { + isReturningUser = value + } +} diff --git a/DuckDuckGoTests/OnboardingAddressBarPositionPickerViewModelTests.swift b/DuckDuckGoTests/OnboardingAddressBarPositionPickerViewModelTests.swift new file mode 100644 index 0000000000..5708195ae5 --- /dev/null +++ b/DuckDuckGoTests/OnboardingAddressBarPositionPickerViewModelTests.swift @@ -0,0 +1,93 @@ +// +// OnboardingAddressBarPositionPickerViewModelTests.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 +@testable import DuckDuckGo + +final class OnboardingAddressBarPositionPickerViewModelTests: XCTestCase { + private var addressBarPositionManagerMock: AddressBarPositionManagerMock! + + override func setUpWithError() throws { + addressBarPositionManagerMock = AddressBarPositionManagerMock() + try super.setUpWithError() + } + + override func tearDownWithError() throws { + addressBarPositionManagerMock = nil + try super.tearDownWithError() + } + + func testWhenInitThenDisplayModelsAreCorrect() throws { + // GIVEN + addressBarPositionManagerMock.currentAddressBarPosition = .top + let sut = OnboardingAddressBarPositionPickerViewModel(addressBarPositionManager: addressBarPositionManagerMock) + + // WHEN + let items = sut.items + + // THEN + let firstItem = try XCTUnwrap(items.first) + XCTAssertEqual(firstItem.type, .top) + XCTAssertEqual(firstItem.title.string, UserText.HighlightsOnboardingExperiment.AddressBarPosition.topTitle + " " + UserText.HighlightsOnboardingExperiment.AddressBarPosition.defaultOption) + XCTAssertEqual(firstItem.message, UserText.HighlightsOnboardingExperiment.AddressBarPosition.topMessage) + XCTAssertEqual(firstItem.icon, .addressBarTop) + XCTAssertTrue(firstItem.isSelected) + + let secondItem = try XCTUnwrap(items.last) + XCTAssertEqual(secondItem.type, .bottom) + XCTAssertEqual(secondItem.title.string, UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomTitle) + XCTAssertEqual(secondItem.message, UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomMessage) + XCTAssertEqual(secondItem.icon, .addressBarBottom) + XCTAssertFalse(secondItem.isSelected) + } + + func testWhenUpdateAddressBarThenDisplayModelsAreUpdated() throws { + // GIVEN + addressBarPositionManagerMock.currentAddressBarPosition = .top + let sut = OnboardingAddressBarPositionPickerViewModel(addressBarPositionManager: addressBarPositionManagerMock) + XCTAssertEqual(sut.items.first?.type, .top) + XCTAssertTrue(sut.items.first?.isSelected ?? false) + + // WHEN + sut.setAddressBar(position: .bottom) + + // THEN + XCTAssertEqual(addressBarPositionManagerMock.currentAddressBarPosition, .bottom) + + let items = sut.items + let firstItem = try XCTUnwrap(items.first) + XCTAssertEqual(firstItem.type, .top) + XCTAssertEqual(firstItem.title.string, UserText.HighlightsOnboardingExperiment.AddressBarPosition.topTitle + " " + UserText.HighlightsOnboardingExperiment.AddressBarPosition.defaultOption) + XCTAssertEqual(firstItem.message, UserText.HighlightsOnboardingExperiment.AddressBarPosition.topMessage) + XCTAssertEqual(firstItem.icon, .addressBarTop) + XCTAssertFalse(firstItem.isSelected) + + let secondItem = try XCTUnwrap(items.last) + XCTAssertEqual(secondItem.type, .bottom) + XCTAssertEqual(secondItem.title.string, UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomTitle) + XCTAssertEqual(secondItem.message, UserText.HighlightsOnboardingExperiment.AddressBarPosition.bottomMessage) + XCTAssertEqual(secondItem.icon, .addressBarBottom) + XCTAssertTrue(secondItem.isSelected) + } + +} + +private class AddressBarPositionManagerMock: AddressBarPositionManaging { + var currentAddressBarPosition: AddressBarPosition = .top +} diff --git a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift index f3ad300002..520ded80cf 100644 --- a/DuckDuckGoTests/OnboardingIntroViewModelTests.swift +++ b/DuckDuckGoTests/OnboardingIntroViewModelTests.swift @@ -21,12 +21,23 @@ import XCTest @testable import DuckDuckGo final class OnboardingIntroViewModelTests: XCTestCase { + private var onboardingManager: OnboardingManagerMock! + + override func setUpWithError() throws { + try super.setUpWithError() + onboardingManager = OnboardingManagerMock() + } + + override func tearDownWithError() throws { + onboardingManager = nil + try super.tearDownWithError() + } // MARK: - State + Actions func testWhenSubscribeToViewStateThenShouldSendLanding() { // GIVEN - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) // WHEN let result = sut.state @@ -37,32 +48,32 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenOnAppearIsCalledThenViewStateChangesToStartOnboardingDialog() { // GIVEN - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertEqual(sut.state, .landing) // WHEN sut.onAppear() // THEN - XCTAssertEqual(sut.state, .onboarding(.startOnboardingDialog)) + XCTAssertEqual(sut.state, .onboarding(.init(type: .startOnboardingDialog, step: .hidden))) } func testWhenStartOnboardingActionIsCalledThenViewStateChangesToBrowsersComparisonDialog() { // GIVEN - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager) XCTAssertEqual(sut.state, .landing) // WHEN sut.startOnboardingAction() // THEN - XCTAssertEqual(sut.state, .onboarding(.browsersComparisonDialog)) + XCTAssertEqual(sut.state, .onboarding(.init(type: .browsersComparisonDialog, step: .hidden))) } func testWhenSetDefaultBrowserActionIsCalledThenURLOpenerOpensSettingsURL() { // GIVEN let urlOpenerMock = MockURLOpener() - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), urlOpener: urlOpenerMock) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: urlOpenerMock) XCTAssertFalse(urlOpenerMock.didCallOpenURL) XCTAssertNil(urlOpenerMock.capturedURL) @@ -77,7 +88,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { // GIVEN var didCallOnCompletingOnboardingIntro = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) sut.onCompletingOnboardingIntro = { didCallOnCompletingOnboardingIntro = true } @@ -93,7 +104,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenCancelSetDefaultBrowserActionIsCalledThenOnCompletingOnboardingIntroIsCalled() { // GIVEN var didCallOnCompletingOnboardingIntro = false - let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) sut.onCompletingOnboardingIntro = { didCallOnCompletingOnboardingIntro = true } @@ -106,12 +117,193 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertTrue(didCallOnCompletingOnboardingIntro) } + // MARK: - Highlights State + Actions iPhone + + // MARK: iPhone + + func testWhenSubscribeToViewStateAndIsHighlightsIphoneFlowThenShouldSendLanding() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + + // WHEN + let result = sut.state + + // THEN + XCTAssertEqual(result, .landing) + } + + func testWhenOnAppearIsCalledAndAndIsHighlightsIphoneFlowThenViewStateChangesToStartOnboardingDialogAndProgressIsHidden() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.onAppear() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .startOnboardingDialog, step: .hidden))) + } + + func testWhenStartOnboardingActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToBrowsersComparisonDialogAndProgressIs1Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.startOnboardingAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .browsersComparisonDialog, step: .init(currentStep: 1, totalSteps: 3)))) + } + + func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 3)))) + } + + func testWhenCancelSetDefaultBrowserActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.cancelSetDefaultBrowserAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 3)))) + } + + func testWhenAppIconPickerContinueActionIsCalledAndIsHighlightsIphoneFlowThenViewStateChangesToChooseAddressBarPositionDialogAndProgressIs3Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.appIconPickerContinueAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAddressBarPositionDialog, step: .init(currentStep: 3, totalSteps: 3)))) + } + + func testWhenSelectAddressBarPositionActionIsCalledAndIsHighlightsIphoneFlowThenOnCompletingOnboardingIntroIsCalled() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + var didCallOnCompletingOnboardingIntro = false + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: false, urlOpener: MockURLOpener()) + sut.onCompletingOnboardingIntro = { + didCallOnCompletingOnboardingIntro = true + } + XCTAssertFalse(didCallOnCompletingOnboardingIntro) + + // WHEN + sut.selectAddressBarPositionAction() + + // THEN + XCTAssertTrue(didCallOnCompletingOnboardingIntro) + } + + // MARK: iPad + + func testWhenSubscribeToViewStateAndIsHighlightsIpadFlowThenShouldSendLanding() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + + // WHEN + let result = sut.state + + // THEN + XCTAssertEqual(result, .landing) + } + + func testWhenOnAppearIsCalledAndAndIsHighlightsIpadFlowThenViewStateChangesToStartOnboardingDialogAndProgressIsHidden() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.onAppear() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .startOnboardingDialog, step: .hidden))) + } + // + func testWhenStartOnboardingActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToBrowsersComparisonDialogAndProgressIs1Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.startOnboardingAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .browsersComparisonDialog, step: .init(currentStep: 1, totalSteps: 2)))) + } + + func testWhenSetDefaultBrowserActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.setDefaultBrowserAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 2)))) + } + + func testWhenCancelSetDefaultBrowserActionIsCalledAndIsHighlightsIpadFlowThenViewStateChangesToChooseAppIconDialogAndProgressIs2Of3() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + XCTAssertEqual(sut.state, .landing) + + // WHEN + sut.cancelSetDefaultBrowserAction() + + // THEN + XCTAssertEqual(sut.state, .onboarding(.init(type: .chooseAppIconDialog, step: .init(currentStep: 2, totalSteps: 2)))) + } + + func testWhenAppIconPickerContinueActionIsCalledAndIsHighlightsIphoneFlowThenOnCompletingOnboardingIntroIsCalled() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + var didCallOnCompletingOnboardingIntro = false + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, isIpad: true, urlOpener: MockURLOpener()) + sut.onCompletingOnboardingIntro = { + didCallOnCompletingOnboardingIntro = true + } + XCTAssertFalse(didCallOnCompletingOnboardingIntro) + + // WHEN + sut.appIconPickerContinueAction() + + // THEN + XCTAssertTrue(didCallOnCompletingOnboardingIntro) + } + // MARK: - Pixels func testWhenOnAppearIsCalledThenPixelReporterTrackOnboardingIntroImpression() { // GIVEN let pixelReporterMock = OnboardingIntroPixelReporterMock() - let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertFalse(pixelReporterMock.didCallTrackOnboardingIntroImpression) // WHEN @@ -124,7 +316,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenStartOnboardingActionIsCalledThenPixelReporterTrackBrowserComparisonImpression() { // GIVEN let pixelReporterMock = OnboardingIntroPixelReporterMock() - let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertFalse(pixelReporterMock.didCallTrackBrowserComparisonImpression) // WHEN @@ -137,7 +329,7 @@ final class OnboardingIntroViewModelTests: XCTestCase { func testWhenChooseBrowserIsCalledThenPixelReporterTrackChooseBrowserCTAAction() { // GIVEN let pixelReporterMock = OnboardingIntroPixelReporterMock() - let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, urlOpener: MockURLOpener()) + let sut = OnboardingIntroViewModel(pixelReporter: pixelReporterMock, onboardingManager: onboardingManager, urlOpener: MockURLOpener()) XCTAssertFalse(pixelReporterMock.didCallTrackChooseBrowserCTAAction) // WHEN @@ -147,6 +339,56 @@ final class OnboardingIntroViewModelTests: XCTestCase { XCTAssertTrue(pixelReporterMock.didCallTrackChooseBrowserCTAAction) } + // MARK: - Copy + + func testWhenIsNotHighlightsThenIntroTitleIsCorrect() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = false + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + + // WHEN + let result = sut.copy.introTitle + + // THEN + XCTAssertEqual(result, UserText.DaxOnboardingExperiment.Intro.title) + } + + func testWhenIsHighlightsThenIntroTitleIsCorrect() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + + // WHEN + let result = sut.copy.introTitle + + // THEN + XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.Intro.title) + } + + func testWhenIsNotHighlightsThenBrowserComparisonTitleIsCorrect() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = false + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + + // WHEN + let result = sut.copy.browserComparisonTitle + + // THEN + XCTAssertEqual(result, UserText.DaxOnboardingExperiment.BrowsersComparison.title) + } + + func testWhenIsHighlightsThenBrowserComparisonTitleIsCorrect() { + // GIVEN + onboardingManager.isOnboardingHighlightsEnabled = true + let sut = OnboardingIntroViewModel(pixelReporter: OnboardingIntroPixelReporterMock(), onboardingManager: onboardingManager, urlOpener: MockURLOpener()) + + // WHEN + let result = sut.copy.browserComparisonTitle + + // THEN + XCTAssertEqual(result, UserText.HighlightsOnboardingExperiment.BrowsersComparison.title) + } + } private final class OnboardingIntroPixelReporterMock: OnboardingIntroPixelReporting { diff --git a/DuckDuckGoTests/OnboardingManagerMock.swift b/DuckDuckGoTests/OnboardingManagerMock.swift new file mode 100644 index 0000000000..9322299bfe --- /dev/null +++ b/DuckDuckGoTests/OnboardingManagerMock.swift @@ -0,0 +1,25 @@ +// +// OnboardingManagerMock.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 +@testable import DuckDuckGo + +final class OnboardingManagerMock: OnboardingHighlightsManaging { + var isOnboardingHighlightsEnabled: Bool = false +} diff --git a/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift b/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift index fbf73bf6ff..59456d6edc 100644 --- a/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift +++ b/DuckDuckGoTests/OnboardingSuggestedSearchesProviderTests.swift @@ -22,12 +22,23 @@ import Onboarding @testable import DuckDuckGo class OnboardingSuggestedSearchesProviderTests: XCTestCase { - + private var onboardingManagerMock: OnboardingManagerMock! let userText = UserText.DaxOnboardingExperiment.ContextualOnboarding.self + let highlightsUserText = UserText.HighlightsOnboardingExperiment.ContextualOnboarding.self + + override func setUpWithError() throws { + try super.setUpWithError() + onboardingManagerMock = OnboardingManagerMock() + } + + override func tearDownWithError() throws { + onboardingManagerMock = nil + try super.tearDownWithError() + } func testSearchesListForEnglishLanguageAndUsRegion() { let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "en") - let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) let expectedSearches = [ ContextualOnboardingListItem.search(title: userText.tryASearchOption1English), @@ -41,7 +52,7 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { func testSearchesListForNonEnglishLanguageAndNonUSRegion() { let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "fr", languageCode: "fr") - let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) let expectedSearches = [ ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), @@ -55,7 +66,7 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { func testSearchesListForUSRegionAndNonEnglishLanguage() { let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "es") - let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) let expectedSearches = [ ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), @@ -66,6 +77,51 @@ class OnboardingSuggestedSearchesProviderTests: XCTestCase { XCTAssertEqual(provider.list, expectedSearches) } + + // MARK: - Higlights Experiment + + func testWhenHighlightsOnboardingAndSearchesListForEnglishLanguageAndUsRegionThenDoNotReturnOption3() { + onboardingManagerMock.isOnboardingHighlightsEnabled = true + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "en") + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) + + let expectedSearches = [ + ContextualOnboardingListItem.search(title: userText.tryASearchOption1English), + ContextualOnboardingListItem.search(title: userText.tryASearchOption2English), + ContextualOnboardingListItem.surprise(title: highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") + ] + + XCTAssertEqual(provider.list, expectedSearches) + } + + func testWhenHighlightsOnboardingAndSearchesListForNonEnglishLanguageAndNonUSRegionThenDoNotReturnOption3() { + onboardingManagerMock.isOnboardingHighlightsEnabled = true + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "fr", languageCode: "fr") + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) + + let expectedSearches = [ + ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), + ContextualOnboardingListItem.search(title: userText.tryASearchOption2International), + ContextualOnboardingListItem.surprise(title: highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") + ] + + XCTAssertEqual(provider.list, expectedSearches) + } + + func testWhenHighlightsOnboardingAndSearchesListForUSRegionAndNonEnglishLanguageThenDoNotReturnOption3() { + onboardingManagerMock.isOnboardingHighlightsEnabled = true + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "es") + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider, onboardingManager: onboardingManagerMock) + + let expectedSearches = [ + ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), + ContextualOnboardingListItem.search(title: userText.tryASearchOption2English), + ContextualOnboardingListItem.surprise(title: highlightsUserText.tryASearchOptionSurpriseMe, visibleTitle: "Surprise me!") + ] + + XCTAssertEqual(provider.list, expectedSearches) + } + } class MockOnboardingRegionAndLanguageProvider: OnboardingRegionAndLanguageProvider { diff --git a/DuckDuckGoTests/PrivacyIconLogicTests.swift b/DuckDuckGoTests/PrivacyIconLogicTests.swift index ebecb5d55c..b12ef295e8 100644 --- a/DuckDuckGoTests/PrivacyIconLogicTests.swift +++ b/DuckDuckGoTests/PrivacyIconLogicTests.swift @@ -96,7 +96,7 @@ class PrivacyIconLogicTests: XCTestCase { let url = PrivacyIconLogicTests.pageURL let entity = Entity(displayName: "E", domains: [], prevalence: 100.0) let protectionStatus = ProtectionStatus(unprotectedTemporary: false, enabledFeatures: [], allowlisted: false, denylisted: false) - let privacyInfo = PrivacyInfo(url: url, parentEntity: entity, protectionStatus: protectionStatus) + let privacyInfo = PrivacyInfo(url: url, parentEntity: entity, protectionStatus: protectionStatus, shouldCheckServerTrust: true) let icon = PrivacyIconLogic.privacyIcon(for: privacyInfo) @@ -105,6 +105,19 @@ class PrivacyIconLogicTests: XCTestCase { XCTAssertEqual(icon, .shieldWithDot) } + func testPrivacyIconIsShieldWithoutDotForNoSecTrustAndShouldCheckServerTrustIsFalse() { + let url = PrivacyIconLogicTests.pageURL + let entity = Entity(displayName: "E", domains: [], prevalence: 100.0) + let protectionStatus = ProtectionStatus(unprotectedTemporary: false, enabledFeatures: [], allowlisted: false, denylisted: false) + let privacyInfo = PrivacyInfo(url: url, parentEntity: entity, protectionStatus: protectionStatus) + + let icon = PrivacyIconLogic.privacyIcon(for: privacyInfo) + + XCTAssertTrue(url.isHttps) + XCTAssertTrue(privacyInfo.https) + XCTAssertEqual(icon, .shield) + } + } final class MockSecTrust: SecurityTrust {} diff --git a/DuckDuckGoTests/WebCacheManagerTests.swift b/DuckDuckGoTests/WebCacheManagerTests.swift index 2a40a5bea2..94c1f7dd20 100644 --- a/DuckDuckGoTests/WebCacheManagerTests.swift +++ b/DuckDuckGoTests/WebCacheManagerTests.swift @@ -20,18 +20,15 @@ import XCTest @testable import Core import WebKit +import TestUtils class WebCacheManagerTests: XCTestCase { - var userDefaults = UserDefaults(suiteName: "test")! - override func setUp() { super.setUp() CookieStorage().cookies = [] CookieStorage().isConsumed = true - userDefaults.removeSuite(named: "test") - UserDefaults.standard.removeObject(forKey: UserDefaultsWrapper.Key.webContainerId.rawValue) if #available(iOS 17, *) { WKWebsiteDataStore.fetchAllDataStoreIdentifiers { uuids in uuids.forEach { @@ -47,21 +44,19 @@ class WebCacheManagerTests: XCTestCase { let logins = MockPreservedLogins(domains: []) let storage = CookieStorage() - let inMemoryDataStoreIdManager = DataStoreIdManager() - XCTAssertFalse(inMemoryDataStoreIdManager.hasId) - XCTAssertNil(inMemoryDataStoreIdManager.id) + let inMemoryDataStoreIdManager = DataStoreIdManager(store: MockKeyValueStore()) + XCTAssertNil(inMemoryDataStoreIdManager.currentId) await WebCacheManager.shared.clear(cookieStorage: storage, logins: logins, dataStoreIdManager: inMemoryDataStoreIdManager) - XCTAssertTrue(inMemoryDataStoreIdManager.hasId) - let oldId = inMemoryDataStoreIdManager.id?.uuidString + XCTAssertNotNil(inMemoryDataStoreIdManager.currentId) + let oldId = inMemoryDataStoreIdManager.currentId?.uuidString XCTAssertNotNil(oldId) await WebCacheManager.shared.clear(cookieStorage: storage, logins: logins, dataStoreIdManager: inMemoryDataStoreIdManager) - XCTAssertTrue(inMemoryDataStoreIdManager.hasId) - XCTAssertNotNil(inMemoryDataStoreIdManager.id?.uuidString) - XCTAssertNotEqual(inMemoryDataStoreIdManager.id?.uuidString, oldId) + XCTAssertNotNil(inMemoryDataStoreIdManager.currentId) + XCTAssertNotEqual(inMemoryDataStoreIdManager.currentId?.uuidString, oldId) } @available(iOS 17, *) @@ -87,7 +82,7 @@ class WebCacheManagerTests: XCTestCase { XCTAssertEqual(5, loadedCount) let cookieStore = CookieStorage() - await WebCacheManager.shared.clear(cookieStorage: cookieStore, logins: logins, dataStoreIdManager: DataStoreIdManager.shared) + await WebCacheManager.shared.clear(cookieStorage: cookieStore, logins: logins, dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) let cookies = await defaultStore.httpCookieStore.allCookies() XCTAssertEqual(cookies.count, 0) @@ -136,7 +131,7 @@ class WebCacheManagerTests: XCTestCase { await WebCacheManager.shared.clear(cookieStorage: cookieStorage, logins: logins, - dataStoreIdManager: DataStoreIdManager.shared) + dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) let cookies = await defaultStore.httpCookieStore.allCookies() XCTAssertEqual(cookies.count, 0) @@ -166,7 +161,7 @@ class WebCacheManagerTests: XCTestCase { let storage = CookieStorage() storage.isConsumed = true - await WebCacheManager.shared.clear(cookieStorage: storage, logins: logins, dataStoreIdManager: DataStoreIdManager.shared) + await WebCacheManager.shared.clear(cookieStorage: storage, logins: logins, dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) XCTAssertEqual(storage.cookies.count, 2) XCTAssertTrue(storage.cookies.contains(where: { $0.domain == "duckduckgo.com" })) @@ -193,7 +188,7 @@ class WebCacheManagerTests: XCTestCase { let cookieStore = CookieStorage() - await WebCacheManager.shared.clear(cookieStorage: cookieStore, logins: logins, dataStoreIdManager: DataStoreIdManager.shared) + await WebCacheManager.shared.clear(cookieStorage: cookieStore, logins: logins, dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) let cookies = await defaultStore.httpCookieStore.allCookies() XCTAssertEqual(cookies.count, 0) @@ -201,9 +196,9 @@ class WebCacheManagerTests: XCTestCase { XCTAssertEqual(1, cookieStore.cookies.count) XCTAssertEqual(cookieStore.cookies[0].domain, "www.example.com") } - + @MainActor - func testWhenAccessingObservationsDbThenValidDatabasePoolIsReturned() { + func x_testWhenAccessingObservationsDbThenValidDatabasePoolIsReturned() { let pool = WebCacheManager.shared.getValidDatabasePool() XCTAssertNotNil(pool, "DatabasePool should not be nil") } @@ -225,16 +220,3 @@ class WebCacheManagerTests: XCTestCase { } } - -class InMemoryDataStoreIdManager: DataStoreIdManaging { - - var id: UUID? - - var hasId: Bool { - id != nil - } - - func allocateNewContainerId() { - id = UUID() - } -} diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index db4fdc06b9..b64d694bb6 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -318,6 +318,7 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { } @objc init() { + APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) let settings = VPNSettings(defaults: .networkProtectionGroupDefaults)