Skip to content

Commit

Permalink
Merge branch 'main' into sam/persistent-pixels
Browse files Browse the repository at this point in the history
* main:
  Remove VPN feature flag checks (#3334)
  Add Marketplace Postback handling (#3357)
  SKAD4 crash fix (#3361)
  Enroll all internal users in experiment && Update BSK (#3359)
  update for macOS: visited links (#3353)
  • Loading branch information
samsymons committed Sep 16, 2024
2 parents 175ae48 + 98d546f commit 9866e5a
Show file tree
Hide file tree
Showing 24 changed files with 493 additions and 158 deletions.
6 changes: 3 additions & 3 deletions Core/HistoryManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -137,8 +137,8 @@ class NullHistoryCoordinator: HistoryCoordinating {
completion()
}

func burnDomains(_ baseDomains: Set<String>, tld: Common.TLD, completion: @escaping () -> Void) {
completion()
func burnDomains(_ baseDomains: Set<String>, tld: Common.TLD, completion: @escaping (Set<URL>) -> Void) {
completion([])
}

func burnVisits(_ visits: [History.Visit], completion: @escaping () -> Void) {
Expand Down
93 changes: 93 additions & 0 deletions Core/MarketplaceAdPostback.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
74 changes: 74 additions & 0 deletions Core/MarketplaceAdPostbackManager.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
62 changes: 62 additions & 0 deletions Core/MarketplaceAdPostbackStorage.swift
Original file line number Diff line number Diff line change
@@ -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) }
}
}
81 changes: 81 additions & 0 deletions Core/MarketplaceAdPostbackUpdater.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
}
6 changes: 3 additions & 3 deletions Core/SyncErrorHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,21 @@ public enum AsyncErrorType: String {

public class SyncErrorHandler: EventMapping<SyncError> {
@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()
}
Expand Down
Loading

0 comments on commit 9866e5a

Please sign in to comment.