Skip to content

Commit

Permalink
Merge branch 'main' into daniel/bsk.update
Browse files Browse the repository at this point in the history
  • Loading branch information
afterxleep authored Sep 16, 2024
2 parents c0d4697 + 35b7a45 commit a32c8b1
Show file tree
Hide file tree
Showing 77 changed files with 1,287 additions and 811 deletions.
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")
}
}
}
}
Loading

0 comments on commit a32c8b1

Please sign in to comment.