From d5d9fbe6bafc31cfb85bc6dc5872e5bb1bd4b749 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 13 Sep 2024 13:14:12 +0500 Subject: [PATCH 1/5] update for macOS: visited links (#3353) Task/Issue URL: https://app.asana.com/0/1175293949586521/1204211850543327/f BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/992 macOS PR: https://github.com/duckduckgo/macos-browser/pull/3261 --- Core/HistoryManager.swift | 6 +++--- Core/SyncErrorHandler.swift | 6 +++--- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) 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/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/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9ece20c422..e4d39f99ee 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10853,7 +10853,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 193.2.1; + version = 194.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a54a4f059b..55a375cacb 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "e304d397d61f74a43453748bdc86f933e3fe5425", - "version" : "193.2.1" + "revision" : "026acbd36fb80c95e0bfc6a9080e369dd85db66f", + "version" : "194.0.0" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", + "location" : "https://github.com/apple/swift-argument-parser", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" From 1c736e9c4d9befca0a42ed280b4d47c2a5f38800 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Fri, 13 Sep 2024 12:37:44 +0200 Subject: [PATCH 2/5] Enroll all internal users in experiment && Update BSK (#3359) Task/Issue URL: https://app.asana.com/0/1204099484721401/1208292211771741/f BSK PR: duckduckgo/BrowserServicesKit#994 Description: We want all internal users enrolled in the experiment Update in BSK to patch Privacy Config, so 'internal' is translated to 'enabled' when privacyConfig is passed to the FE --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/AutofillLoginListViewModel.swift | 2 +- .../DuckPlayer/DuckPlayerLaunchExperiment.swift | 12 ++++++++++-- DuckDuckGo/ImportPasswordsViewModel.swift | 2 +- DuckDuckGo/SpeechRecognizer.swift | 2 +- .../TabViewControllerBrowsingMenuExtension.swift | 2 +- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index e4d39f99ee..d9f651fc28 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10853,7 +10853,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 194.0.0; + version = 194.1.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 55a375cacb..c5522d379b 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "026acbd36fb80c95e0bfc6a9080e369dd85db66f", - "version" : "194.0.0" + "revision" : "09b4901eeab71625c4796c0819d0066278b7b6d6", + "version" : "194.1.0" } }, { diff --git a/DuckDuckGo/AutofillLoginListViewModel.swift b/DuckDuckGo/AutofillLoginListViewModel.swift index 5c2ff7a615..0bf99e6fe1 100644 --- a/DuckDuckGo/AutofillLoginListViewModel.swift +++ b/DuckDuckGo/AutofillLoginListViewModel.swift @@ -229,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 } diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift b/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift index b809c7d5b9..19decaf548 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerLaunchExperiment.swift @@ -91,6 +91,8 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { @UserDefaultsWrapper(key: .duckPlayerPixelExperimentCohort, defaultValue: nil) var experimentCohort: String? + private var isInternalUser: Bool + enum Cohort: String { case control case experiment @@ -100,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)? { @@ -140,7 +144,11 @@ final class DuckPlayerLaunchExperiment: DuckPlayerLaunchExperimentHandling { 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() 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/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/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 From 7be8bf67f10241a206556f612ca399b846672755 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Fri, 13 Sep 2024 16:11:24 +0100 Subject: [PATCH 3/5] SKAD4 crash fix (#3361) Task/Issue URL: https://app.asana.com/0/414709148257752/1208303110742725/f Fix for a logging string interpolation in SKAD4 attribution. I think this could be an internal AdAttributionKit error because every thrown error should be not nil and responding to `localisedDescription`, but here looks like something is going wrong when we try to log `Logger.general.error("Attribution: SKAN 4 postback failed \(error.localizedDescription, privacy: .public)")` and accessing `error.localizedDescription ` This attempt tries not to assume anything about the error and just logs a string representation. --- DuckDuckGo/AppDelegate+Attribution.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/AppDelegate+Attribution.swift b/DuckDuckGo/AppDelegate+Attribution.swift index e82dd002e3..bfef272bf4 100644 --- a/DuckDuckGo/AppDelegate+Attribution.swift +++ b/DuckDuckGo/AppDelegate+Attribution.swift @@ -43,7 +43,7 @@ extension AppDelegate { try await AdAttributionKit.Postback.updateConversionValue(conversionValue, coarseConversionValue: .high, lockPostback: true) Logger.general.debug("Attribution: AdAttributionKit postback succeeded") } catch { - Logger.general.error("Attribution: AdAttributionKit postback failed \(error.localizedDescription, privacy: .public)") + Logger.general.error("Attribution: AdAttributionKit postback failed \(String(describing: error), privacy: .public)") } } @@ -52,8 +52,8 @@ extension AppDelegate { do { try await SKAdNetwork.updatePostbackConversionValue(conversionValue, coarseValue: .high, lockWindow: true) Logger.general.debug("Attribution: SKAN 4 postback succeeded") - } catch { - Logger.general.error("Attribution: SKAN 4 postback failed \(error.localizedDescription, privacy: .public)") + } catch let error { + Logger.general.error("Attribution: SKAN 4 postback failed \(String(describing: error), privacy: .public)") } } From 283411894ef58ed258e0ad80f09228927d7f8eb2 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 13 Sep 2024 20:55:02 +0100 Subject: [PATCH 4/5] Add Marketplace Postback handling (#3357) Task/Issue URL: https://app.asana.com/0/72649045549333/1208126219488944/f Tech Design URL: https://app.asana.com/0/72649045549333/1208274586553401/f **Description**: Add Marketplace Postback handling --- Core/MarketplaceAdPostback.swift | 93 +++++++++++++++++ Core/MarketplaceAdPostbackManager.swift | 74 ++++++++++++++ Core/MarketplaceAdPostbackStorage.swift | 62 ++++++++++++ Core/MarketplaceAdPostbackUpdater.swift | 81 +++++++++++++++ DuckDuckGo.xcodeproj/project.pbxproj | 40 +++++++- DuckDuckGo/AppDelegate+Attribution.swift | 60 ----------- DuckDuckGo/AppDelegate.swift | 17 +++- DuckDuckGo/Info.plist | 2 + .../MarketplaceAdPostbackManagerTests.swift | 99 +++++++++++++++++++ 9 files changed, 460 insertions(+), 68 deletions(-) create mode 100644 Core/MarketplaceAdPostback.swift create mode 100644 Core/MarketplaceAdPostbackManager.swift create mode 100644 Core/MarketplaceAdPostbackStorage.swift create mode 100644 Core/MarketplaceAdPostbackUpdater.swift delete mode 100644 DuckDuckGo/AppDelegate+Attribution.swift create mode 100644 DuckDuckGoTests/MarketplaceAdPostbackManagerTests.swift 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/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d9f651fc28..d751612bbe 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 */; }; @@ -1065,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 */; }; @@ -1433,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 = ""; }; @@ -1446,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 = ""; }; @@ -2891,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 = ""; }; @@ -3514,6 +3522,14 @@ name = LoginDetails; sourceTree = ""; }; + 316790E32C9350980090B0A2 /* MarketplaceAdPostback */ = { + isa = PBXGroup; + children = ( + 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */, + ); + name = MarketplaceAdPostback; + sourceTree = ""; + }; 316931DA27BD24B60095F5ED /* Alerts */ = { isa = PBXGroup; children = ( @@ -3564,6 +3580,17 @@ 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 = ( @@ -5774,6 +5801,7 @@ F143C2E51E4A4CD400CFDE3A /* Core */ = { isa = PBXGroup; children = ( + 31B2F10D2C92FEB000CD30E3 /* MarketplaceAdPostback */, F1CE42A71ECA0A520074A8DF /* Bookmarks */, 837774491F8E1ECE00E17A29 /* ContentBlocker */, F143C2E61E4A4CD400CFDE3A /* Core.h */, @@ -6097,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 */, @@ -6242,6 +6269,7 @@ F1E092B31E92A6B900732CCC /* Core */ = { isa = PBXGroup; children = ( + 316790E32C9350980090B0A2 /* MarketplaceAdPostback */, 858479CA2B8795BF00D156C1 /* History */, EA7EFE662677F5BD0075464E /* PrivacyReferenceTests */, 83EDCC3E1F86B363005CDFCD /* API */, @@ -7330,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 */, @@ -7920,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 */, @@ -8100,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 */, @@ -8134,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 */, @@ -8158,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 */, @@ -8184,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 */, diff --git a/DuckDuckGo/AppDelegate+Attribution.swift b/DuckDuckGo/AppDelegate+Attribution.swift deleted file mode 100644 index bfef272bf4..0000000000 --- a/DuckDuckGo/AppDelegate+Attribution.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// AppDelegate+Attribution.swift -// DuckDuckGo -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Common -import StoreKit -import AdAttributionKit -import os.log - -extension AppDelegate { - - func updateAttribution(conversionValue: Int) { - Task { - if #available(iOS 17.4, *) { - // https://developer.apple.com/documentation/adattributionkit/adattributionkit-skadnetwork-interoperability - await updateAdAttributionKitPostback(conversionValue: conversionValue) - await updateSKANPostback(conversionValue: conversionValue) - } else if #available(iOS 16.1, *) { - await updateSKANPostback(conversionValue: conversionValue) - } - } - } - - @available(iOS 17.4, *) - private func updateAdAttributionKitPostback(conversionValue: Int) async { - do { - try await AdAttributionKit.Postback.updateConversionValue(conversionValue, coarseConversionValue: .high, lockPostback: true) - 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(conversionValue: Int) async { - do { - try await SKAdNetwork.updatePostbackConversionValue(conversionValue, coarseValue: .high, lockWindow: true) - Logger.general.debug("Attribution: SKAN 4 postback succeeded") - } catch let error { - Logger.general.error("Attribution: SKAN 4 postback failed \(String(describing: error), privacy: .public)") - } - } - -} diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 9bd1828a58..50b89c4358 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -104,6 +104,7 @@ import os.log private let launchOptionsHandler = LaunchOptionsHandler() private let onboardingPixelReporter = OnboardingPixelReporter() + private let marketplaceAdPostbackManager = MarketplaceAdPostbackManager() override init() { super.init() @@ -117,9 +118,6 @@ import os.log // swiftlint:disable:next function_body_length func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Attribution support - updateAttribution(conversionValue: 1) - #if targetEnvironment(simulator) if ProcessInfo.processInfo.environment["UITESTING"] == "true" { // Disable hardware keyboards. @@ -523,6 +521,7 @@ import os.log } AppConfigurationFetch().start { result in + self.sendAppLaunchPostback() if case .assetsUpdated(let protectionsUpdated) = result, protectionsUpdated { ContentBlocking.shared.contentBlockingManager.scheduleCompilation() } @@ -757,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() @@ -772,6 +779,9 @@ import os.log // New users don't see the message historyMessageManager.dismiss() + + // Setup storage for marketplace postback + marketplaceAdPostbackManager.updateReturningUserValue() } } @@ -968,7 +978,6 @@ import os.log UIApplication.shared.shortcutItems = nil } } - } extension AppDelegate: BlankSnapshotViewRecoveringDelegate { 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/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 + } +} From 98d546fa90d1dfcb68ec103332dad29510014c9d Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 13 Sep 2024 16:07:53 -0700 Subject: [PATCH 5/5] Remove VPN feature flag checks (#3334) Task/Issue URL: https://app.asana.com/0/414235014887631/1208254974939835/f Tech Design URL: CC: Description: This PR cleans up VPN feature flag checks after the subscription launch. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +-- DuckDuckGo/AppDependencyProvider.swift | 15 +---------- .../DefaultNetworkProtectionVisibility.swift | 27 ++----------------- ...orkProtectionConvenienceInitialisers.swift | 18 +------------ ...NetworkProtectionDebugViewController.swift | 3 --- .../NetworkProtectionFeatureVisibility.swift | 7 +---- DuckDuckGo/SettingsViewModel.swift | 12 +++------ ...etworkProtectionPacketTunnelProvider.swift | 1 - 9 files changed, 12 insertions(+), 77 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d751612bbe..632258f36e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10885,7 +10885,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 194.1.0; + version = 195.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c5522d379b..d785772d67 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "09b4901eeab71625c4796c0819d0066278b7b6d6", - "version" : "194.1.0" + "revision" : "f9134f887b1215779a1050134d09d7e824a8abc0", + "version" : "195.0.0" } }, { diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 2577e88ea6..a6c5e6b403 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -125,25 +125,12 @@ class AppDependencyProvider: DependencyProvider { privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, purchasePlatform: .appStore) let accessTokenProvider: () -> String? = { - func isSubscriptionEnabled() -> Bool { -#if ALPHA || DEBUG - if let subscriptionOverrideEnabled = UserDefaults.networkProtectionGroupDefaults.subscriptionOverrideEnabled { - return subscriptionOverrideEnabled - } -#endif - return subscriptionFeatureAvailability.isFeatureAvailable - } - - if isSubscriptionEnabled() { - return { accountManager.accessToken } - } - return { nil } + return { accountManager.accessToken } }() #if os(macOS) networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(keychainType: .dataProtection(.unspecified), serviceName: "\(Bundle.main.bundleIdentifier!).authToken", errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: true, accessTokenProvider: accessTokenProvider) #else networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(accessTokenProvider: accessTokenProvider) diff --git a/DuckDuckGo/DefaultNetworkProtectionVisibility.swift b/DuckDuckGo/DefaultNetworkProtectionVisibility.swift index 29f988a2b3..baa0cebf53 100644 --- a/DuckDuckGo/DefaultNetworkProtectionVisibility.swift +++ b/DuckDuckGo/DefaultNetworkProtectionVisibility.swift @@ -34,33 +34,10 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { } var token: String? { - if shouldMonitorEntitlement() { - return accountManager.accessToken - } - return nil - } - - func isPrivacyProLaunched() -> Bool { - if let subscriptionOverrideEnabled = userDefaults.subscriptionOverrideEnabled { -#if ALPHA || DEBUG - return subscriptionOverrideEnabled -#else - return false -#endif - } - - return AppDependencyProvider.shared.subscriptionFeatureAvailability.isFeatureAvailable - } - - func shouldMonitorEntitlement() -> Bool { - isPrivacyProLaunched() + return accountManager.accessToken } func shouldShowVPNShortcut() -> Bool { - if isPrivacyProLaunched() { - return accountManager.isUserAuthenticated - } else { - return false - } + return accountManager.isUserAuthenticated } } diff --git a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift index d44f9dbc10..e440446044 100644 --- a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift +++ b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift @@ -55,21 +55,6 @@ extension ConnectionServerInfoObserverThroughSession { } } -extension NetworkProtectionCodeRedemptionCoordinator { - - convenience init(isManualCodeRedemptionFlow: Bool = false, accountManager: AccountManager) { - let settings = AppDependencyProvider.shared.vpnSettings - let networkProtectionVisibility = AppDependencyProvider.shared.vpnFeatureVisibility - self.init( - environment: settings.selectedEnvironment, - tokenStore: AppDependencyProvider.shared.networkProtectionKeychainTokenStore, - isManualCodeRedemptionFlow: isManualCodeRedemptionFlow, - errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: networkProtectionVisibility.isPrivacyProLaunched() - ) - } -} - extension NetworkProtectionVPNSettingsViewModel { convenience init() { self.init( @@ -86,8 +71,7 @@ extension NetworkProtectionLocationListCompositeRepository { self.init( environment: settings.selectedEnvironment, tokenStore: AppDependencyProvider.shared.networkProtectionKeychainTokenStore, - errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: AppDependencyProvider.shared.vpnFeatureVisibility.isPrivacyProLaunched() + errorEvents: .networkProtectionAppDebugEvents ) } } diff --git a/DuckDuckGo/NetworkProtectionDebugViewController.swift b/DuckDuckGo/NetworkProtectionDebugViewController.swift index c48a3e55f3..9af0df4f9f 100644 --- a/DuckDuckGo/NetworkProtectionDebugViewController.swift +++ b/DuckDuckGo/NetworkProtectionDebugViewController.swift @@ -634,9 +634,6 @@ final class NetworkProtectionDebugViewController: UITableViewController { cell.textLabel?.text = """ Endpoint: \(AppDependencyProvider.shared.vpnSettings.selectedEnvironment.endpointURL.absoluteString) -isPrivacyProLaunched: \(vpnVisibility.isPrivacyProLaunched() ? "YES" : "NO") - -shouldMonitorEntitlement: \(vpnVisibility.shouldMonitorEntitlement() ? "YES" : "NO") shouldShowVPNShortcut: \(vpnVisibility.shouldShowVPNShortcut() ? "YES" : "NO") """ case .none: diff --git a/DuckDuckGo/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/NetworkProtectionFeatureVisibility.swift index 7bcff5c708..854dcdc9de 100644 --- a/DuckDuckGo/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/NetworkProtectionFeatureVisibility.swift @@ -21,13 +21,8 @@ import Foundation import Subscription public protocol NetworkProtectionFeatureVisibility { - func isPrivacyProLaunched() -> Bool - - /// Whether to enforce entitlement check and show entitlement-related messaging - /// This should always happen after 100% roll out - /// N.B. Backend will independently check for valid entitlement regardless of this value - func shouldMonitorEntitlement() -> Bool /// Whether to show VPN shortcut on the home screen func shouldShowVPNShortcut() -> Bool + } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index da6ae1abb2..8dbc850832 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -464,14 +464,10 @@ extension SettingsViewModel { } private func updateNetPStatus(connectionStatus: ConnectionStatus) { - if AppDependencyProvider.shared.vpnFeatureVisibility.isPrivacyProLaunched() { - switch connectionStatus { - case .connected: - self.state.networkProtectionConnected = true - default: - self.state.networkProtectionConnected = false - } - } else { + switch connectionStatus { + case .connected: + self.state.networkProtectionConnected = true + default: self.state.networkProtectionConnected = false } } diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 71069a06fe..b64d694bb6 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -374,7 +374,6 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { providerEvents: Self.packetTunnelProviderEvents, settings: settings, defaults: .networkProtectionGroupDefaults, - isSubscriptionEnabled: true, entitlementCheck: { return await Self.entitlementCheck(accountManager: accountManager) }) accountManager.delegate = self