Skip to content

Commit 043a7f3

Browse files
Always prepend idfa to targeting identifiers (#54)
Co-authored-by: Eugene Dorfman <eugene.dorfman@gmail.com>
1 parent 3d4d11b commit 043a7f3

13 files changed

Lines changed: 564 additions & 50 deletions

File tree

OptableSDK.xcodeproj/project.pbxproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
Unit/LocalStorageTests.swift,
4848
Unit/OptableIdentifierEncoderTests.swift,
4949
Unit/OptableIdentifiersTests.swift,
50+
Unit/OptableSDKHelpersIdentifiersEnrichmentTests.swift,
51+
Unit/OptableSDKHelpersTests.swift,
5052
);
5153
target = 6352AB0324EAD403002E66EB /* OptableSDKTests */;
5254
};
@@ -382,6 +384,7 @@
382384
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
383385
SKIP_INSTALL = YES;
384386
SUPPORTS_MACCATALYST = NO;
387+
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
385388
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
386389
SWIFT_VERSION = 5.0;
387390
TARGETED_DEVICE_FAMILY = "1,2";

OptableSDK.xcodeproj/xcshareddata/xcschemes/OptableSDKTests.xcscheme

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,23 @@
1313
buildForProfiling = "NO"
1414
buildForArchiving = "NO"
1515
buildForAnalyzing = "YES">
16-
<AutocreatedTestPlanReference>
17-
</AutocreatedTestPlanReference>
16+
<TestPlanReference
17+
reference = "container:Tests/OptableSDKTests.xctestplan">
18+
</TestPlanReference>
1819
</BuildActionEntry>
1920
</BuildActionEntries>
2021
</BuildAction>
2122
<TestAction
2223
buildConfiguration = "Debug"
2324
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
2425
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
25-
shouldUseLaunchSchemeArgsEnv = "YES"
26-
shouldAutocreateTestPlan = "YES">
26+
shouldUseLaunchSchemeArgsEnv = "YES">
27+
<TestPlans>
28+
<TestPlanReference
29+
reference = "container:Tests/OptableSDKTests.xctestplan"
30+
default = "YES">
31+
</TestPlanReference>
32+
</TestPlans>
2733
<Testables>
2834
<TestableReference
2935
skipped = "NO">

Source/Misc/AppTrackingTransparency.swift

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,47 @@
1616
import Foundation
1717

1818
enum ATT {
19-
static var advertisingIdentifier: UUID {
20-
ASIdentifierManager.shared().advertisingIdentifier
21-
}
19+
// MARK: advertisingIdentifier
2220

23-
@available(iOS, introduced: 6, deprecated: 14, message: "This has been replaced by functionality in AppTrackingTransparency's ATTrackingManager class.")
24-
static var isAdvertisingTrackingEnabled: Bool {
25-
ASIdentifierManager.shared().isAdvertisingTrackingEnabled
26-
}
21+
#if DEBUG
22+
static var advertisingIdentifier_DebugOverride: UUID?
23+
static var advertisingIdentifier: UUID {
24+
advertisingIdentifier_DebugOverride ?? ASIdentifierManager.shared().advertisingIdentifier
25+
}
26+
#else
27+
static var advertisingIdentifier: UUID {
28+
ASIdentifierManager.shared().advertisingIdentifier
29+
}
30+
#endif
31+
32+
// MARK: isAdvertisingTrackingEnabled
33+
34+
#if DEBUG
35+
@available(iOS, introduced: 6, deprecated: 14,
36+
message: "Replaced by ATTrackingManager in AppTrackingTransparency.")
37+
static var isAdvertisingTrackingEnabled_DebugOverride: Bool?
38+
static var isAdvertisingTrackingEnabled: Bool {
39+
isAdvertisingTrackingEnabled_DebugOverride ?? ASIdentifierManager.shared().isAdvertisingTrackingEnabled
40+
}
41+
#else
42+
static var isAdvertisingTrackingEnabled: Bool {
43+
ASIdentifierManager.shared().isAdvertisingTrackingEnabled
44+
}
45+
#endif
46+
47+
// MARK: advertisingIdentifierAvailable
48+
49+
#if DEBUG
50+
static var advertisingIdentifierAvailable_DebugOverride: Bool?
51+
#endif
2752

2853
static var advertisingIdentifierAvailable: Bool {
54+
#if DEBUG
55+
if let override = advertisingIdentifierAvailable_DebugOverride {
56+
return override
57+
}
58+
#endif
59+
2960
#if canImport(AppTrackingTransparency)
3061
if #available(iOS 14, *) {
3162
return trackingStatus == .authorized
@@ -36,8 +67,20 @@
3667
return isAdvertisingTrackingEnabled
3768
#endif
3869
}
39-
70+
71+
// MARK: attAvailable
72+
73+
#if DEBUG
74+
static var attAvailable_DebugOverride: Bool?
75+
#endif
76+
4077
static var attAvailable: Bool {
78+
#if DEBUG
79+
if let override = attAvailable_DebugOverride {
80+
return override
81+
}
82+
#endif
83+
4184
if #available(iOS 14, *) {
4285
return true
4386
} else {
@@ -47,38 +90,74 @@
4790

4891
#if canImport(AppTrackingTransparency)
4992

93+
// MARK: canAuthorize
94+
95+
#if DEBUG
96+
@available(iOS 14, *)
97+
static var canAuthorize_DebugOverride: Bool?
98+
#endif
99+
50100
static var canAuthorize: Bool {
51101
if #available(iOS 14, *) {
102+
#if DEBUG
103+
if let override = canAuthorize_DebugOverride {
104+
return override
105+
}
106+
#endif
107+
52108
return ATTrackingManager.trackingAuthorizationStatus == .notDetermined
53109
} else {
54110
return false
55111
}
56112
}
57113

114+
// MARK: trackingStatus
115+
116+
#if DEBUG
117+
@available(iOS 14, *)
118+
static var trackingStatus_DebugOverride: ATTrackingManager.AuthorizationStatus?
119+
#endif
120+
58121
@available(iOS 14, *)
59122
static var trackingStatus: ATTrackingManager.AuthorizationStatus {
60-
ATTrackingManager.trackingAuthorizationStatus
123+
#if DEBUG
124+
return trackingStatus_DebugOverride ?? ATTrackingManager.trackingAuthorizationStatus
125+
#else
126+
return ATTrackingManager.trackingAuthorizationStatus
127+
#endif
61128
}
62129

130+
// MARK: RequestAuthorization
131+
63132
@available(iOS 14, *)
64133
static func requestATTAuthorization(completion: ((Bool) -> Void)? = nil) {
134+
#if DEBUG
135+
if let override = trackingStatus_DebugOverride {
136+
completion?(override == .authorized)
137+
return
138+
}
139+
#endif
140+
65141
ATTrackingManager.requestTrackingAuthorization { status in
66142
switch status {
67-
case .authorized: completion?(true)
68-
case .denied, .notDetermined, .restricted: completion?(false)
69-
@unknown default: completion?(true)
143+
case .authorized:
144+
completion?(true)
145+
case .denied, .notDetermined, .restricted:
146+
completion?(false)
147+
@unknown default:
148+
completion?(true)
70149
}
71150
}
72151
}
73152

74153
@available(iOS 14, *)
75154
@discardableResult
76155
static func requestATTAuthorization() async -> Bool {
77-
await withCheckedContinuation({ continuation in
78-
requestATTAuthorization(completion: { isAuthorized in
156+
await withCheckedContinuation { continuation in
157+
requestATTAuthorization { isAuthorized in
79158
continuation.resume(returning: isAuthorized)
80-
})
81-
})
159+
}
160+
}
82161
}
83162

84163
#endif
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// RangeReplaceableCollection+Compat.swift
3+
// OptableSDK
4+
//
5+
// Copyright © 2026 Optable Technologies, Inc. All rights reserved.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
11+
extension RangeReplaceableCollection where Self: MutableCollection, Index == Int {
12+
mutating func removeCompat(atOffsets offsets: IndexSet) {
13+
if #available(iOS 13.0, *) {
14+
remove(atOffsets: offsets)
15+
} else {
16+
// Remove from highest index to lowest to avoid shifting issues
17+
for index in offsets.sorted(by: >) {
18+
remove(at: index)
19+
}
20+
}
21+
}
22+
}

Source/OptableSDK.swift

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,11 @@ public class OptableSDK: NSObject {
5252
let config: OptableConfig
5353
let api: EdgeAPI
5454

55-
/// Initializes the SDK with the provided OptableConfig. On iOS 14+, requests tracking authorization unless skipAdvertisingIdDetection is true.
55+
/// Initializes the SDK with the provided OptableConfig.
5656
@objc
5757
public init(config: OptableConfig) {
5858
self.config = config
5959
self.api = EdgeAPI(config)
60-
61-
// Automatically request Tracking Authorization
62-
if #available(iOS 14, *) {
63-
if config.skipAdvertisingIdDetection == false, ATT.canAuthorize {
64-
ATT.requestATTAuthorization()
65-
}
66-
}
6760
}
6861

6962
/// OptableSDK version
@@ -98,7 +91,7 @@ public extension OptableSDK {
9891
}
9992
```
10093
*/
101-
func identify(_ ids: [OptableIdentifier], _ completion: @escaping (Result<HTTPURLResponse, Error>) -> Void) throws {
94+
func identify(_ ids: [OptableIdentifier], completion: @escaping (Result<HTTPURLResponse, Error>) -> Void) throws {
10295
try _identify(ids, completion: completion)
10396
}
10497

@@ -327,8 +320,8 @@ public extension OptableSDK {
327320
}
328321
}
329322

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

@@ -360,7 +353,7 @@ private extension OptableSDK {
360353
var ids = ids ?? []
361354

362355
enrichIfNeeded(ids: &ids)
363-
356+
364357
guard let request = try api.targeting(ids: ids) else {
365358
throw OptableError.targeting("Failed to create targeting request")
366359
}
@@ -448,19 +441,29 @@ private extension OptableSDK {
448441
}
449442
}).resume()
450443
}
451-
452-
private func enrichIfNeeded(ids: inout [OptableIdentifier]) {
444+
445+
func enrichIfNeeded(ids: inout [OptableIdentifier]) {
453446
// Enrich with Apple IDFA
454447
if config.skipAdvertisingIdDetection == false,
455448
ATT.advertisingIdentifierAvailable,
456-
ATT.advertisingIdentifier != UUID(uuid: uuid_t(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)),
457-
ids.contains(where: { eid in
458-
if case let .appleIDFA(value) = eid, value.isEmpty == false {
459-
return true
460-
}
461-
return false
462-
}) == false {
463-
ids.append(.appleIDFA(ATT.advertisingIdentifier.uuidString))
449+
ATT.advertisingIdentifier != UUID(uuid: uuid_t(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) {
450+
let systemIDFA = ATT.advertisingIdentifier.uuidString
451+
452+
var idfaMatchingSystemIdxs: [Int] = []
453+
454+
for idx in ids.indices {
455+
if case let .appleIDFA(value) = ids[idx] {
456+
if value == systemIDFA {
457+
idfaMatchingSystemIdxs.append(idx)
458+
}
459+
}
460+
}
461+
462+
// Remove all matching systemIDFA (deduplicate)
463+
ids.removeCompat(atOffsets: IndexSet(idfaMatchingSystemIdxs))
464+
465+
// Prepend all identifiers with systemIDFA
466+
ids.insert(.appleIDFA(systemIDFA), at: ids.startIndex)
464467
}
465468
}
466469

Source/Public/ObjCSupport/OptableSDKIdentifier.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010

1111
#import <Foundation/Foundation.h>
1212

13-
//#import <OptableSDK/OptableSDKIdentifierType.h>
14-
1513
NS_ASSUME_NONNULL_BEGIN
1614

1715
typedef NS_ENUM(NSInteger, OptableSDKIdentifierType) {

Tests/Integration/OptableSDKTests.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,30 @@ class OptableSDKTests: XCTestCase {
7676
try sdk.targeting([OptableSDKIdentifier(type: .emailAddress, value: "test@test.com", customIdx: nil)])
7777
wait(for: [targetExpectation], timeout: 10)
7878
}
79+
80+
func test_targetingFromCache_and_targetingClearCache() {
81+
let config = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK)
82+
let sdk = OptableSDK(config: config)
83+
84+
// Seed storage directly
85+
let expected = OptableTargeting(
86+
optableTargeting: ["foo": "bar"],
87+
gamTargetingKeywords: ["ks": "id1,id2"],
88+
ortb2: "{\"user\":{}}"
89+
)
90+
sdk.api.storage.setTargeting(expected)
91+
92+
// Read through SDK wrapper
93+
let fromCache = sdk.targetingFromCache()
94+
XCTAssertNotNil(fromCache)
95+
XCTAssertEqual(fromCache!.targetingData as? [String: String], ["foo": "bar"])
96+
XCTAssertEqual(fromCache!.gamTargetingKeywords as? [String: String], ["ks": "id1,id2"])
97+
XCTAssertEqual(fromCache!.ortb2, "{\"user\":{}}")
98+
99+
// Clear and verify empty
100+
sdk.targetingClearCache()
101+
XCTAssertNil(sdk.targetingFromCache())
102+
}
79103

80104
// MARK: Witness
81105
@available(iOS 13.0, *)

Tests/OptableSDKTests.xctestplan

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"configurations" : [
3+
{
4+
"id" : "CA98C897-F222-4E1B-9D66-8D550B33E18E",
5+
"name" : "Test Scheme Action",
6+
"options" : {
7+
8+
}
9+
}
10+
],
11+
"defaultOptions" : {
12+
"performanceAntipatternCheckerEnabled" : true
13+
},
14+
"testTargets" : [
15+
{
16+
"target" : {
17+
"containerPath" : "container:OptableSDK.xcodeproj",
18+
"identifier" : "6352AB0324EAD403002E66EB",
19+
"name" : "OptableSDKTests"
20+
}
21+
}
22+
],
23+
"version" : 1
24+
}

0 commit comments

Comments
 (0)