Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions OptableSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
Unit/LocalStorageTests.swift,
Unit/OptableIdentifierEncoderTests.swift,
Unit/OptableIdentifiersTests.swift,
Unit/OptableSDKHelpersIdentifiersEnrichmentTests.swift,
Unit/OptableSDKHelpersTests.swift,
);
target = 6352AB0324EAD403002E66EB /* OptableSDKTests */;
};
Expand Down Expand Up @@ -382,6 +384,7 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,23 @@
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<AutocreatedTestPlanReference>
</AutocreatedTestPlanReference>
<TestPlanReference
reference = "container:Tests/OptableSDKTests.xctestplan">
</TestPlanReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:Tests/OptableSDKTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">
Expand Down
111 changes: 95 additions & 16 deletions Source/Misc/AppTrackingTransparency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,47 @@
import Foundation

enum ATT {
static var advertisingIdentifier: UUID {
ASIdentifierManager.shared().advertisingIdentifier
}
// MARK: advertisingIdentifier

@available(iOS, introduced: 6, deprecated: 14, message: "This has been replaced by functionality in AppTrackingTransparency's ATTrackingManager class.")
static var isAdvertisingTrackingEnabled: Bool {
ASIdentifierManager.shared().isAdvertisingTrackingEnabled
}
#if DEBUG
static var advertisingIdentifier_DebugOverride: UUID?
static var advertisingIdentifier: UUID {
advertisingIdentifier_DebugOverride ?? ASIdentifierManager.shared().advertisingIdentifier
}
#else
static var advertisingIdentifier: UUID {
ASIdentifierManager.shared().advertisingIdentifier
}
#endif

// MARK: isAdvertisingTrackingEnabled

#if DEBUG
@available(iOS, introduced: 6, deprecated: 14,
message: "Replaced by ATTrackingManager in AppTrackingTransparency.")
static var isAdvertisingTrackingEnabled_DebugOverride: Bool?
static var isAdvertisingTrackingEnabled: Bool {
isAdvertisingTrackingEnabled_DebugOverride ?? ASIdentifierManager.shared().isAdvertisingTrackingEnabled
}
#else
static var isAdvertisingTrackingEnabled: Bool {
ASIdentifierManager.shared().isAdvertisingTrackingEnabled
}
#endif

// MARK: advertisingIdentifierAvailable

#if DEBUG
static var advertisingIdentifierAvailable_DebugOverride: Bool?
#endif

static var advertisingIdentifierAvailable: Bool {
#if DEBUG
if let override = advertisingIdentifierAvailable_DebugOverride {
return override
}
#endif

#if canImport(AppTrackingTransparency)
if #available(iOS 14, *) {
return trackingStatus == .authorized
Expand All @@ -36,8 +67,20 @@
return isAdvertisingTrackingEnabled
#endif
}


// MARK: attAvailable

#if DEBUG
static var attAvailable_DebugOverride: Bool?
#endif

static var attAvailable: Bool {
#if DEBUG
if let override = attAvailable_DebugOverride {
return override
}
#endif

if #available(iOS 14, *) {
return true
} else {
Expand All @@ -47,38 +90,74 @@

#if canImport(AppTrackingTransparency)

// MARK: canAuthorize

#if DEBUG
@available(iOS 14, *)
static var canAuthorize_DebugOverride: Bool?
#endif

static var canAuthorize: Bool {
if #available(iOS 14, *) {
#if DEBUG
if let override = canAuthorize_DebugOverride {
return override
}
#endif

return ATTrackingManager.trackingAuthorizationStatus == .notDetermined
} else {
return false
}
}

// MARK: trackingStatus

#if DEBUG
@available(iOS 14, *)
static var trackingStatus_DebugOverride: ATTrackingManager.AuthorizationStatus?
#endif

@available(iOS 14, *)
static var trackingStatus: ATTrackingManager.AuthorizationStatus {
ATTrackingManager.trackingAuthorizationStatus
#if DEBUG
return trackingStatus_DebugOverride ?? ATTrackingManager.trackingAuthorizationStatus
#else
return ATTrackingManager.trackingAuthorizationStatus
#endif
}

// MARK: RequestAuthorization

@available(iOS 14, *)
static func requestATTAuthorization(completion: ((Bool) -> Void)? = nil) {
#if DEBUG
if let override = trackingStatus_DebugOverride {
completion?(override == .authorized)
return
}
#endif

ATTrackingManager.requestTrackingAuthorization { status in
switch status {
case .authorized: completion?(true)
case .denied, .notDetermined, .restricted: completion?(false)
@unknown default: completion?(true)
case .authorized:
completion?(true)
case .denied, .notDetermined, .restricted:
completion?(false)
@unknown default:
completion?(true)
}
}
}

@available(iOS 14, *)
@discardableResult
static func requestATTAuthorization() async -> Bool {
await withCheckedContinuation({ continuation in
requestATTAuthorization(completion: { isAuthorized in
await withCheckedContinuation { continuation in
requestATTAuthorization { isAuthorized in
continuation.resume(returning: isAuthorized)
})
})
}
}
}

#endif
Expand Down
22 changes: 22 additions & 0 deletions Source/Misc/RangeReplaceableCollection+Compat.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// RangeReplaceableCollection+Compat.swift
// OptableSDK
//
// Copyright © 2026 Optable Technologies, Inc. All rights reserved.
//

import Foundation
import SwiftUI

extension RangeReplaceableCollection where Self: MutableCollection, Index == Int {
mutating func removeCompat(atOffsets offsets: IndexSet) {
if #available(iOS 13.0, *) {
remove(atOffsets: offsets)
} else {
// Remove from highest index to lowest to avoid shifting issues
for index in offsets.sorted(by: >) {
remove(at: index)
}
}
}
}
47 changes: 25 additions & 22 deletions Source/OptableSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,11 @@ public class OptableSDK: NSObject {
let config: OptableConfig
let api: EdgeAPI

/// Initializes the SDK with the provided OptableConfig. On iOS 14+, requests tracking authorization unless skipAdvertisingIdDetection is true.
/// Initializes the SDK with the provided OptableConfig.
@objc
public init(config: OptableConfig) {
self.config = config
self.api = EdgeAPI(config)

// Automatically request Tracking Authorization
if #available(iOS 14, *) {
if config.skipAdvertisingIdDetection == false, ATT.canAuthorize {
ATT.requestATTAuthorization()
}
}
}

/// OptableSDK version
Expand Down Expand Up @@ -98,7 +91,7 @@ public extension OptableSDK {
}
```
*/
func identify(_ ids: [OptableIdentifier], _ completion: @escaping (Result<HTTPURLResponse, Error>) -> Void) throws {
func identify(_ ids: [OptableIdentifier], completion: @escaping (Result<HTTPURLResponse, Error>) -> Void) throws {
try _identify(ids, completion: completion)
}

Expand Down Expand Up @@ -327,8 +320,8 @@ public extension OptableSDK {
}
}

// MARK: - Private
private extension OptableSDK {
// MARK: - Internal
extension OptableSDK {
func _identify(_ ids: [OptableIdentifier], completion: @escaping (Result<HTTPURLResponse, Error>) -> Void) throws {
var ids = ids

Expand Down Expand Up @@ -360,7 +353,7 @@ private extension OptableSDK {
var ids = ids ?? []

enrichIfNeeded(ids: &ids)

guard let request = try api.targeting(ids: ids) else {
throw OptableError.targeting("Failed to create targeting request")
}
Expand Down Expand Up @@ -448,19 +441,29 @@ private extension OptableSDK {
}
}).resume()
}
private func enrichIfNeeded(ids: inout [OptableIdentifier]) {

func enrichIfNeeded(ids: inout [OptableIdentifier]) {
// Enrich with Apple IDFA
if config.skipAdvertisingIdDetection == false,
ATT.advertisingIdentifierAvailable,
ATT.advertisingIdentifier != UUID(uuid: uuid_t(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)),
ids.contains(where: { eid in
if case let .appleIDFA(value) = eid, value.isEmpty == false {
return true
}
return false
}) == false {
ids.append(.appleIDFA(ATT.advertisingIdentifier.uuidString))
ATT.advertisingIdentifier != UUID(uuid: uuid_t(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) {
let systemIDFA = ATT.advertisingIdentifier.uuidString

var idfaMatchingSystemIdxs: [Int] = []

for idx in ids.indices {
if case let .appleIDFA(value) = ids[idx] {
if value == systemIDFA {
idfaMatchingSystemIdxs.append(idx)
}
}
}

// Remove all matching systemIDFA (deduplicate)
ids.removeCompat(atOffsets: IndexSet(idfaMatchingSystemIdxs))

// Prepend all identifiers with systemIDFA
ids.insert(.appleIDFA(systemIDFA), at: ids.startIndex)
}
}

Expand Down
2 changes: 0 additions & 2 deletions Source/Public/ObjCSupport/OptableSDKIdentifier.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@

#import <Foundation/Foundation.h>

//#import <OptableSDK/OptableSDKIdentifierType.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSInteger, OptableSDKIdentifierType) {
Expand Down
24 changes: 24 additions & 0 deletions Tests/Integration/OptableSDKTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,30 @@ class OptableSDKTests: XCTestCase {
try sdk.targeting([OptableSDKIdentifier(type: .emailAddress, value: "test@test.com", customIdx: nil)])
wait(for: [targetExpectation], timeout: 10)
}

func test_targetingFromCache_and_targetingClearCache() {
let config = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK)
let sdk = OptableSDK(config: config)

// Seed storage directly
let expected = OptableTargeting(
optableTargeting: ["foo": "bar"],
gamTargetingKeywords: ["ks": "id1,id2"],
ortb2: "{\"user\":{}}"
)
sdk.api.storage.setTargeting(expected)

// Read through SDK wrapper
let fromCache = sdk.targetingFromCache()
XCTAssertNotNil(fromCache)
XCTAssertEqual(fromCache!.targetingData as? [String: String], ["foo": "bar"])
XCTAssertEqual(fromCache!.gamTargetingKeywords as? [String: String], ["ks": "id1,id2"])
XCTAssertEqual(fromCache!.ortb2, "{\"user\":{}}")

// Clear and verify empty
sdk.targetingClearCache()
XCTAssertNil(sdk.targetingFromCache())
}

// MARK: Witness
@available(iOS 13.0, *)
Expand Down
24 changes: 24 additions & 0 deletions Tests/OptableSDKTests.xctestplan
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"configurations" : [
{
"id" : "CA98C897-F222-4E1B-9D66-8D550B33E18E",
"name" : "Test Scheme Action",
"options" : {

}
}
],
"defaultOptions" : {
"performanceAntipatternCheckerEnabled" : true
},
"testTargets" : [
{
"target" : {
"containerPath" : "container:OptableSDK.xcodeproj",
"identifier" : "6352AB0324EAD403002E66EB",
"name" : "OptableSDKTests"
}
}
],
"version" : 1
}
Loading
Loading