From aaad82352f940d342f7f6e2b6b387a4f281e3a3a Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Wed, 4 Mar 2026 16:46:07 +0100 Subject: [PATCH 1/5] - Unified `_ completion` parameter for different API parts; Updated doc-comments --- Source/OptableSDK.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index 8588677..e7a71e2 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -85,12 +85,13 @@ public extension OptableSDK { It is asynchronous, and on completion it will call the specified completion handler, passing it either the HTTPURLResponse on success, or an NSError on failure. + ```swift // Example - optableSDK.identify(.init(emailAddress: "example@example.com", phoneNumber: "1234567890"), completion) + optableSDK.identify([.emailAddress("example@example.com"), .phoneNumber("1234567890")], completion: completion) ``` */ - func identify(_ ids: [OptableIdentifier], _ completion: @escaping (Result) -> Void) throws { + func identify(_ ids: [OptableIdentifier], completion: @escaping (Result) -> Void) throws { try _identify(ids, completion: completion) } @@ -141,6 +142,11 @@ public extension OptableSDK { On success, this method will also cache the resulting targeting data in client storage, which can be access using targetingFromCache(), and cleared using targetingClearCache(). + + ```swift + // Example + optableSDK.targeting([.emailAddress("example@example.com"), .phoneNumber("1234567890")], completion: completion) + ``` */ func targeting(_ ids: [OptableIdentifier]? = nil, completion: @escaping (Result) -> Void) throws { try _targeting(ids: ids, completion: completion) @@ -203,7 +209,7 @@ public extension OptableSDK { The witness method is asynchronous, and on completion it will call the specified completion handler, passing it either the HTTPURLResponse on success, or an NSError on failure. */ - func witness(event: String, properties: NSDictionary, _ completion: @escaping (Result) -> Void) throws { + func witness(event: String, properties: NSDictionary, completion: @escaping (Result) -> Void) throws { try _witness(event: event, properties: properties, completion: completion) } @@ -251,7 +257,7 @@ public extension OptableSDK { The specified NSDictionary 'traits' can be subsequently used for audience assembly. The profile method is asynchronous, and on completion it will call the specified completion handler, passing it either the HTTPURLResponse on success, or an NSError on failure. */ - func profile(traits: NSDictionary, id: String? = nil, neighbors: [String]? = nil, _ completion: @escaping (Result) -> Void) throws { + func profile(traits: NSDictionary, id: String? = nil, neighbors: [String]? = nil, completion: @escaping (Result) -> Void) throws { try _profile(traits: traits, id: id, neighbors: neighbors, completion: completion) } From cf324540004b53956fbd6523aa472350c3ef4b16 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Wed, 4 Mar 2026 16:46:57 +0100 Subject: [PATCH 2/5] - Updated OptableIdentifiers enrichment --- OptableSDK.xcodeproj/project.pbxproj | 2 + Source/Misc/AppTrackingTransparency.swift | 111 +++++++++-- .../RangeReplaceableCollection+Compat.swift | 22 +++ Source/OptableSDK.swift | 41 +++-- .../Public/ObjCSupport/OptableSDKIdentifier.h | 2 - Tests/Integration/OptableSDKTests.swift | 4 +- .../Unit/OptableIdentifiersEnrichTests.swift | 174 ++++++++++++++++++ Tests/Unit/OptableIdentifiersTests.swift | 9 +- 8 files changed, 328 insertions(+), 37 deletions(-) create mode 100644 Source/Misc/RangeReplaceableCollection+Compat.swift create mode 100644 Tests/Unit/OptableIdentifiersEnrichTests.swift diff --git a/OptableSDK.xcodeproj/project.pbxproj b/OptableSDK.xcodeproj/project.pbxproj index 917d1f8..4aad47c 100644 --- a/OptableSDK.xcodeproj/project.pbxproj +++ b/OptableSDK.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ Unit/EdgeAPITests.swift, Unit/LocalStorageTests.swift, Unit/OptableIdentifierEncoderTests.swift, + Unit/OptableIdentifiersEnrichTests.swift, Unit/OptableIdentifiersTests.swift, ); target = 6352AB0324EAD403002E66EB /* OptableSDKTests */; @@ -382,6 +383,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"; diff --git a/Source/Misc/AppTrackingTransparency.swift b/Source/Misc/AppTrackingTransparency.swift index b2115f3..5139c9d 100644 --- a/Source/Misc/AppTrackingTransparency.swift +++ b/Source/Misc/AppTrackingTransparency.swift @@ -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 @@ -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 { @@ -47,26 +90,62 @@ #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) } } } @@ -74,11 +153,11 @@ @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 diff --git a/Source/Misc/RangeReplaceableCollection+Compat.swift b/Source/Misc/RangeReplaceableCollection+Compat.swift new file mode 100644 index 0000000..329a242 --- /dev/null +++ b/Source/Misc/RangeReplaceableCollection+Compat.swift @@ -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) + } + } + } +} diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index e7a71e2..ea3b53c 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -318,8 +318,8 @@ public extension OptableSDK { } } -// MARK: - Private -private extension OptableSDK { +// MARK: - Internal +extension OptableSDK { func _identify(_ ids: [OptableIdentifier], completion: @escaping (Result) -> Void) throws { var ids = ids @@ -351,7 +351,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") } @@ -439,19 +439,34 @@ 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 idfaIdxs: [Int] = [] + var idfaMatchingSystemIdxs: [Int] = [] + + for idx in ids.indices { + if case let .appleIDFA(value) = ids[idx] { + idfaIdxs.append(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) + + // TODO: [high] Resolve should remove all others idfa not matching systemIDFA? + // ids.removeCompat(atOffsets: IndexSet(idfaIdxs).subtracting(IndexSet(idfaMatchingSystemIdxs))) } } diff --git a/Source/Public/ObjCSupport/OptableSDKIdentifier.h b/Source/Public/ObjCSupport/OptableSDKIdentifier.h index 1967926..4077165 100644 --- a/Source/Public/ObjCSupport/OptableSDKIdentifier.h +++ b/Source/Public/ObjCSupport/OptableSDKIdentifier.h @@ -10,8 +10,6 @@ #import -//#import - NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, OptableSDKIdentifierType) { diff --git a/Tests/Integration/OptableSDKTests.swift b/Tests/Integration/OptableSDKTests.swift index d4bd350..d2392cc 100644 --- a/Tests/Integration/OptableSDKTests.swift +++ b/Tests/Integration/OptableSDKTests.swift @@ -87,7 +87,7 @@ class OptableSDKTests: XCTestCase { func test_witness_callbacks() throws { let expectation = expectation(description: "witness-callback-expectation") - try sdk.witness(event: "test", properties: ["integration-test-witness": "integration-test-witness-value"], { result in + try sdk.witness(event: "test", properties: ["integration-test-witness": "integration-test-witness-value"], completion: { result in switch result { case let .success(response): XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor")) @@ -114,7 +114,7 @@ class OptableSDKTests: XCTestCase { func test_profile_callbacks() throws { let expectation = expectation(description: "profile-callback-expectation") - try sdk.profile(traits: ["integration-test-profile": "integration-test-profile-value"], { result in + try sdk.profile(traits: ["integration-test-profile": "integration-test-profile-value"], completion: { result in switch result { case let .success(response): XCTAssert(response.targetingData.allKeys.isEmpty == false) diff --git a/Tests/Unit/OptableIdentifiersEnrichTests.swift b/Tests/Unit/OptableIdentifiersEnrichTests.swift new file mode 100644 index 0000000..21f9eb1 --- /dev/null +++ b/Tests/Unit/OptableIdentifiersEnrichTests.swift @@ -0,0 +1,174 @@ +// +// OptableIdentifiersEnrichTests.swift +// OptableSDK +// +// Copyright © 2026 Optable Technologies, Inc. All rights reserved. +// + +import XCTest + +@testable import OptableSDK + +private let systemIDFA: String = "9A8C574D-0B13-45B3-AC67-7CA9C8851920" +private let userIDFA: String = "7F51D71F-3D94-436D-B3FE-CEF646011359" +private let userIDFA_2: String = "543769CE-8339-4502-8D1F-4764008C5C37" + +// MARK: - OptableIdentifiersEnrichTests +class OptableIdentifiersEnrichTests: XCTestCase { + func test_enrich_user_idfa_prepend() { + var identifiers = buildIdentifiers() + identifiers.append(.appleIDFA(userIDFA)) + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = true + sdk.enrichIfNeeded(ids: &identifiers) + + if case let .appleIDFA(value) = identifiers[0] { + XCTAssert(value == userIDFA, "prepended idfa is not the same as user provided") + } else { + XCTFail("user idfa is not prepended") + } + } + + func test_enrich_user_idfas_prepend() { + var identifiers = buildIdentifiers() + identifiers.append(.appleIDFA(userIDFA)) + identifiers.append(.appleIDFA(userIDFA_2)) + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = true + sdk.enrichIfNeeded(ids: &identifiers) + + if case let .appleIDFA(value) = identifiers[0] { + XCTAssert(value == userIDFA, "prepended idfa is not the same as user provided") + } else { + XCTFail("user idfa is not prepended") + } + + if case let .appleIDFA(value) = identifiers[1] { + XCTAssert(value == userIDFA_2, "prepended idfa is not the same as user provided") + } else { + XCTFail("user idfa is not prepended") + } + } + + func test_enrich_system_idfa_prepend() { + ATT.advertisingIdentifierAvailable_DebugOverride = true + ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) + + var identifiers = buildIdentifiers() + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = false + sdk.enrichIfNeeded(ids: &identifiers) + + if case let .appleIDFA(value) = identifiers[0] { + XCTAssert(value == systemIDFA, "prepended idfa is not the same as user provided") + } else { + XCTFail("user idfa is not prepended") + } + } + + @available(iOS 14, *) + func test_enrich_system_idfa_same_as_user_idfa_prepend() { + ATT.advertisingIdentifierAvailable_DebugOverride = true + ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) + + var identifiers = buildIdentifiers() + identifiers.append(.appleIDFA(systemIDFA)) + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = false + sdk.enrichIfNeeded(ids: &identifiers) + + if case let .appleIDFA(value) = identifiers[0] { + XCTAssert(value == systemIDFA, "prepended idfa is not the same as system provided") + } else { + XCTFail("system idfa is not prepended") + } + + if case .appleIDFA = identifiers[1] { + XCTFail("duplicated idfa-s") + } else if identifiers.count(where: { if case .appleIDFA = $0 { return true } else { return false } }) > 1 { + XCTFail("duplicated idfa-s") + } + } + + @available(iOS 14, *) + func test_enrich_system_idfa_user_idfa_prepend() { + ATT.advertisingIdentifierAvailable_DebugOverride = true + ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) + + var identifiers = buildIdentifiers() + identifiers.append(.appleIDFA(userIDFA)) + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = false + sdk.enrichIfNeeded(ids: &identifiers) + + if case let .appleIDFA(value) = identifiers[0] { + XCTAssert(value == systemIDFA, "prepended idfa is not the same as system provided") + } else { + XCTFail("system idfa is not prepended") + } + + if case let .appleIDFA(value) = identifiers[1] { + XCTAssert(value == userIDFA, "prepended idfa is not the same as user provided") + } else { + XCTFail("user idfa is not prepended") + } + } + + func test_enrich_system_idfa_user_idfas_prepend() { + ATT.advertisingIdentifierAvailable_DebugOverride = true + ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) + + var identifiers = buildIdentifiers() + identifiers.append(.appleIDFA(userIDFA)) + identifiers.append(.appleIDFA(userIDFA_2)) + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = false + sdk.enrichIfNeeded(ids: &identifiers) + + if case let .appleIDFA(value) = identifiers[0] { + XCTAssert(value == systemIDFA, "prepended idfa is not the same as user provided") + } else { + XCTFail("user idfa is not prepended") + } + + if case let .appleIDFA(value) = identifiers[1] { + XCTAssert(value == userIDFA, "prepended idfa is not the same as user provided") + } else { + XCTFail("user idfa is not prepended") + } + + if case let .appleIDFA(value) = identifiers[2] { + XCTAssert(value == userIDFA_2, "prepended idfa is not the same as user provided") + } else { + XCTFail("user idfa is not prepended") + } + } + + // MARK: Builders + + func buildSDK() -> OptableSDK { + return OptableSDK(config: OptableConfig( + tenant: T.api.tenant.prebidtest, + originSlug: T.api.slug.iosSDK, + insecure: false, + customUserAgent: T.api.userAgent, + skipAdvertisingIdDetection: true + )) + } + + func buildIdentifiers() -> [OptableIdentifier] { + [ + .emailAddress("test@test.com"), + .phoneNumber("1234567890"), + .postalCode("12345"), + .ipv4Address("127.0.0.1"), + .ipv6Address("2001:db8::7"), + ] + } +} diff --git a/Tests/Unit/OptableIdentifiersTests.swift b/Tests/Unit/OptableIdentifiersTests.swift index 9cedd57..0160427 100644 --- a/Tests/Unit/OptableIdentifiersTests.swift +++ b/Tests/Unit/OptableIdentifiersTests.swift @@ -29,7 +29,7 @@ class OptableIdentifiersTests: XCTestCase { .custom(1, "AaaZza.dh012"), .custom(1, "another c1"), ] - + let encodedData = try JSONEncoder().encode(oids) let decodedData = try JSONDecoder().decode([String].self, from: encodedData) @@ -55,9 +55,10 @@ class OptableIdentifiersTests: XCTestCase { XCTAssertTrue(decodedData.contains(where: { $0 == "c1:another c1" })) // Test order - let c_Idx = decodedData.firstIndex(of: "c:d29c551097b9dd0b82423827f65161232efaf7fc")! - let c1_Idx = decodedData.firstIndex(of: "c1:AaaZza.dh012")! - let c2_Idx = decodedData.firstIndex(of: "c2:")! + + let c_Idx = try XCTUnwrap(decodedData.firstIndex(of: "c:d29c551097b9dd0b82423827f65161232efaf7fc")) + let c1_Idx = try XCTUnwrap(decodedData.firstIndex(of: "c1:AaaZza.dh012")) + let c2_Idx = try XCTUnwrap(decodedData.firstIndex(of: "c2:")) XCTAssert(c_Idx < c2_Idx) XCTAssert(c2_Idx < c1_Idx) From 986c3f54400f09c81cbb450c318c10b982d8d9e7 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Tue, 17 Mar 2026 11:44:06 +0100 Subject: [PATCH 3/5] - Updated Identifier enrichment logic; Added missing unit tests --- OptableSDK.xcodeproj/project.pbxproj | 3 +- .../xcschemes/OptableSDKTests.xcscheme | 9 +- Source/OptableSDK.swift | 5 - Tests/Integration/OptableSDKTests.swift | 24 +++ Tests/OptableSDKTests.xctestplan | 24 +++ .../Unit/OptableIdentifiersEnrichTests.swift | 174 --------------- ...SDKHelpersIdentifiersEnrichmentTests.swift | 203 ++++++++++++++++++ Tests/Unit/OptableSDKHelpersTests.swift | 151 +++++++++++++ 8 files changed, 411 insertions(+), 182 deletions(-) create mode 100644 Tests/OptableSDKTests.xctestplan delete mode 100644 Tests/Unit/OptableIdentifiersEnrichTests.swift create mode 100644 Tests/Unit/OptableSDKHelpersIdentifiersEnrichmentTests.swift create mode 100644 Tests/Unit/OptableSDKHelpersTests.swift diff --git a/OptableSDK.xcodeproj/project.pbxproj b/OptableSDK.xcodeproj/project.pbxproj index 4aad47c..a05613f 100644 --- a/OptableSDK.xcodeproj/project.pbxproj +++ b/OptableSDK.xcodeproj/project.pbxproj @@ -46,8 +46,9 @@ Unit/EdgeAPITests.swift, Unit/LocalStorageTests.swift, Unit/OptableIdentifierEncoderTests.swift, - Unit/OptableIdentifiersEnrichTests.swift, Unit/OptableIdentifiersTests.swift, + Unit/OptableSDKHelpersIdentifiersEnrichmentTests.swift, + Unit/OptableSDKHelpersTests.swift, ); target = 6352AB0324EAD403002E66EB /* OptableSDKTests */; }; diff --git a/OptableSDK.xcodeproj/xcshareddata/xcschemes/OptableSDKTests.xcscheme b/OptableSDK.xcodeproj/xcshareddata/xcschemes/OptableSDKTests.xcscheme index a093cbf..0c24eb9 100644 --- a/OptableSDK.xcodeproj/xcshareddata/xcschemes/OptableSDKTests.xcscheme +++ b/OptableSDK.xcodeproj/xcshareddata/xcschemes/OptableSDKTests.xcscheme @@ -22,8 +22,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - shouldAutocreateTestPlan = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index ea3b53c..359f9b9 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -447,12 +447,10 @@ extension OptableSDK { 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 idfaIdxs: [Int] = [] var idfaMatchingSystemIdxs: [Int] = [] for idx in ids.indices { if case let .appleIDFA(value) = ids[idx] { - idfaIdxs.append(idx) if value == systemIDFA { idfaMatchingSystemIdxs.append(idx) } @@ -464,9 +462,6 @@ extension OptableSDK { // Prepend all identifiers with systemIDFA ids.insert(.appleIDFA(systemIDFA), at: ids.startIndex) - - // TODO: [high] Resolve should remove all others idfa not matching systemIDFA? - // ids.removeCompat(atOffsets: IndexSet(idfaIdxs).subtracting(IndexSet(idfaMatchingSystemIdxs))) } } diff --git a/Tests/Integration/OptableSDKTests.swift b/Tests/Integration/OptableSDKTests.swift index d2392cc..fc71dfc 100644 --- a/Tests/Integration/OptableSDKTests.swift +++ b/Tests/Integration/OptableSDKTests.swift @@ -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, *) diff --git a/Tests/OptableSDKTests.xctestplan b/Tests/OptableSDKTests.xctestplan new file mode 100644 index 0000000..ba969e0 --- /dev/null +++ b/Tests/OptableSDKTests.xctestplan @@ -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 +} diff --git a/Tests/Unit/OptableIdentifiersEnrichTests.swift b/Tests/Unit/OptableIdentifiersEnrichTests.swift deleted file mode 100644 index 21f9eb1..0000000 --- a/Tests/Unit/OptableIdentifiersEnrichTests.swift +++ /dev/null @@ -1,174 +0,0 @@ -// -// OptableIdentifiersEnrichTests.swift -// OptableSDK -// -// Copyright © 2026 Optable Technologies, Inc. All rights reserved. -// - -import XCTest - -@testable import OptableSDK - -private let systemIDFA: String = "9A8C574D-0B13-45B3-AC67-7CA9C8851920" -private let userIDFA: String = "7F51D71F-3D94-436D-B3FE-CEF646011359" -private let userIDFA_2: String = "543769CE-8339-4502-8D1F-4764008C5C37" - -// MARK: - OptableIdentifiersEnrichTests -class OptableIdentifiersEnrichTests: XCTestCase { - func test_enrich_user_idfa_prepend() { - var identifiers = buildIdentifiers() - identifiers.append(.appleIDFA(userIDFA)) - - let sdk = buildSDK() - sdk.config.skipAdvertisingIdDetection = true - sdk.enrichIfNeeded(ids: &identifiers) - - if case let .appleIDFA(value) = identifiers[0] { - XCTAssert(value == userIDFA, "prepended idfa is not the same as user provided") - } else { - XCTFail("user idfa is not prepended") - } - } - - func test_enrich_user_idfas_prepend() { - var identifiers = buildIdentifiers() - identifiers.append(.appleIDFA(userIDFA)) - identifiers.append(.appleIDFA(userIDFA_2)) - - let sdk = buildSDK() - sdk.config.skipAdvertisingIdDetection = true - sdk.enrichIfNeeded(ids: &identifiers) - - if case let .appleIDFA(value) = identifiers[0] { - XCTAssert(value == userIDFA, "prepended idfa is not the same as user provided") - } else { - XCTFail("user idfa is not prepended") - } - - if case let .appleIDFA(value) = identifiers[1] { - XCTAssert(value == userIDFA_2, "prepended idfa is not the same as user provided") - } else { - XCTFail("user idfa is not prepended") - } - } - - func test_enrich_system_idfa_prepend() { - ATT.advertisingIdentifierAvailable_DebugOverride = true - ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) - - var identifiers = buildIdentifiers() - - let sdk = buildSDK() - sdk.config.skipAdvertisingIdDetection = false - sdk.enrichIfNeeded(ids: &identifiers) - - if case let .appleIDFA(value) = identifiers[0] { - XCTAssert(value == systemIDFA, "prepended idfa is not the same as user provided") - } else { - XCTFail("user idfa is not prepended") - } - } - - @available(iOS 14, *) - func test_enrich_system_idfa_same_as_user_idfa_prepend() { - ATT.advertisingIdentifierAvailable_DebugOverride = true - ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) - - var identifiers = buildIdentifiers() - identifiers.append(.appleIDFA(systemIDFA)) - - let sdk = buildSDK() - sdk.config.skipAdvertisingIdDetection = false - sdk.enrichIfNeeded(ids: &identifiers) - - if case let .appleIDFA(value) = identifiers[0] { - XCTAssert(value == systemIDFA, "prepended idfa is not the same as system provided") - } else { - XCTFail("system idfa is not prepended") - } - - if case .appleIDFA = identifiers[1] { - XCTFail("duplicated idfa-s") - } else if identifiers.count(where: { if case .appleIDFA = $0 { return true } else { return false } }) > 1 { - XCTFail("duplicated idfa-s") - } - } - - @available(iOS 14, *) - func test_enrich_system_idfa_user_idfa_prepend() { - ATT.advertisingIdentifierAvailable_DebugOverride = true - ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) - - var identifiers = buildIdentifiers() - identifiers.append(.appleIDFA(userIDFA)) - - let sdk = buildSDK() - sdk.config.skipAdvertisingIdDetection = false - sdk.enrichIfNeeded(ids: &identifiers) - - if case let .appleIDFA(value) = identifiers[0] { - XCTAssert(value == systemIDFA, "prepended idfa is not the same as system provided") - } else { - XCTFail("system idfa is not prepended") - } - - if case let .appleIDFA(value) = identifiers[1] { - XCTAssert(value == userIDFA, "prepended idfa is not the same as user provided") - } else { - XCTFail("user idfa is not prepended") - } - } - - func test_enrich_system_idfa_user_idfas_prepend() { - ATT.advertisingIdentifierAvailable_DebugOverride = true - ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) - - var identifiers = buildIdentifiers() - identifiers.append(.appleIDFA(userIDFA)) - identifiers.append(.appleIDFA(userIDFA_2)) - - let sdk = buildSDK() - sdk.config.skipAdvertisingIdDetection = false - sdk.enrichIfNeeded(ids: &identifiers) - - if case let .appleIDFA(value) = identifiers[0] { - XCTAssert(value == systemIDFA, "prepended idfa is not the same as user provided") - } else { - XCTFail("user idfa is not prepended") - } - - if case let .appleIDFA(value) = identifiers[1] { - XCTAssert(value == userIDFA, "prepended idfa is not the same as user provided") - } else { - XCTFail("user idfa is not prepended") - } - - if case let .appleIDFA(value) = identifiers[2] { - XCTAssert(value == userIDFA_2, "prepended idfa is not the same as user provided") - } else { - XCTFail("user idfa is not prepended") - } - } - - // MARK: Builders - - func buildSDK() -> OptableSDK { - return OptableSDK(config: OptableConfig( - tenant: T.api.tenant.prebidtest, - originSlug: T.api.slug.iosSDK, - insecure: false, - customUserAgent: T.api.userAgent, - skipAdvertisingIdDetection: true - )) - } - - func buildIdentifiers() -> [OptableIdentifier] { - [ - .emailAddress("test@test.com"), - .phoneNumber("1234567890"), - .postalCode("12345"), - .ipv4Address("127.0.0.1"), - .ipv6Address("2001:db8::7"), - ] - } -} diff --git a/Tests/Unit/OptableSDKHelpersIdentifiersEnrichmentTests.swift b/Tests/Unit/OptableSDKHelpersIdentifiersEnrichmentTests.swift new file mode 100644 index 0000000..085cb4a --- /dev/null +++ b/Tests/Unit/OptableSDKHelpersIdentifiersEnrichmentTests.swift @@ -0,0 +1,203 @@ +// +// OptableIdentifiersEnrichTests.swift +// OptableSDK +// +// Copyright © 2026 Optable Technologies, Inc. All rights reserved. +// + +import XCTest + +@testable import OptableSDK + +private let systemIDFA: String = "9A8C574D-0B13-45B3-AC67-7CA9C8851920" +private let userIDFA: String = "7F51D71F-3D94-436D-B3FE-CEF646011359" +private let userIDFA_2: String = "543769CE-8339-4502-8D1F-4764008C5C37" + +// MARK: - OptableSDKHelpersIdentifiersEnrichmentTests +class OptableSDKHelpersIdentifiersEnrichmentTests: XCTestCase { + func test_idfa_detection_disabled_enrich_user_idfa_no_prepend() { + var identifiers = buildIdentifiers() + identifiers.append(.appleIDFA(userIDFA)) + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = true + sdk.enrichIfNeeded(ids: &identifiers) + + if case .appleIDFA(_) = identifiers[0] { + XCTFail("User provided IDFA should not be prepended. (System IDFA is unavailable)") + } + } + + func test_idfa_detection_disabled_enrich_user_idfas_no_prepend() { + var identifiers = buildIdentifiers() + identifiers.append(.appleIDFA(userIDFA)) + identifiers.append(.appleIDFA(userIDFA_2)) + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = true + sdk.enrichIfNeeded(ids: &identifiers) + + if case .appleIDFA(_) = identifiers[0] { + XCTFail("User IDFA should not be prepended. (System IDFA is unavailable)") + } + + if case .appleIDFA(_) = identifiers[1] { + XCTFail("User IDFA should not be prepended. (System IDFA is unavailable)") + } + } + + func test_idfa_detection_enabled_enrich_system_idfa_prepend() { + ATT.advertisingIdentifierAvailable_DebugOverride = true + ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) + + var identifiers = buildIdentifiers() + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = false + sdk.enrichIfNeeded(ids: &identifiers) + + if case let .appleIDFA(value) = identifiers[0] { + XCTAssert(value == systemIDFA, "Prepended IDFA is not the same as system provided") + } else { + XCTFail("System IDFA is not prepended") + } + } + + @available(iOS 14, *) + func test_idfa_detection_enabled_enrich_system_idfa_same_as_user_idfa_prepend() { + ATT.advertisingIdentifierAvailable_DebugOverride = true + ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) + + var identifiers = buildIdentifiers() + identifiers.append(.appleIDFA(systemIDFA)) + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = false + sdk.enrichIfNeeded(ids: &identifiers) + + if case let .appleIDFA(value) = identifiers[0] { + XCTAssert(value == systemIDFA, "Prepended IDFA is not the same as system provided") + } else { + XCTFail("System IDFA is not prepended") + } + + if case .appleIDFA = identifiers[1] { + XCTFail("User IDFA persists and was prepended. (duplicate)") + } + + if case .appleIDFA = identifiers.last { + XCTFail("User IDFA persists. (duplicate)") + } + + if identifiers.count(where: { if case .appleIDFA = $0 { return true } else { return false } }) > 1 { + XCTFail("User IDFA persists. (duplicate)") + } + } + + @available(iOS 14, *) + func test_idfa_detection_enabled_enrich_system_idfa_user_idfa_persist() { + ATT.advertisingIdentifierAvailable_DebugOverride = true + ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) + + var identifiers = buildIdentifiers() + identifiers.append(.appleIDFA(userIDFA)) + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = false + sdk.enrichIfNeeded(ids: &identifiers) + + if case let .appleIDFA(value) = identifiers[0] { + XCTAssert(value == systemIDFA, "Prepended IDFA is not the same as system provided") + } else { + XCTFail("System IDFA is not prepended") + } + + if identifiers.contains(where: { + if case let .appleIDFA(value) = $0 { return value == userIDFA } else { return false } + }) { + if case let .appleIDFA(value) = identifiers.last { + XCTAssert(value == userIDFA, "Persisted User IDFA is not the same as user provided") + } else { + XCTFail("Persisted User IDFA is not on the correct position. (should be last)") + } + } else { + XCTFail("User IDFA not persists") + } + } + + func test_idfa_detection_enabled_enrich_system_idfa_user_idfas_persist() { + ATT.advertisingIdentifierAvailable_DebugOverride = true + ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) + + var identifiers = buildIdentifiers() + identifiers.append(.appleIDFA(userIDFA)) + identifiers.append(.appleIDFA(userIDFA_2)) + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = false + sdk.enrichIfNeeded(ids: &identifiers) + + if case let .appleIDFA(value) = identifiers[0] { + XCTAssert(value == systemIDFA, "Prepended IDFA is not the same as system provided") + } else { + XCTFail("System IDFA is not prepended") + } + + let filteredIdentifiers = identifiers.filter({ + if case let .appleIDFA(value) = $0 { + return value == userIDFA || value == userIDFA_2 + } else { return false } + }) + + if filteredIdentifiers.count == 2 { + if case let .appleIDFA(value1) = identifiers[identifiers.count - 2], + case let .appleIDFA(value2) = identifiers[identifiers.count - 1] { + XCTAssert(value1 == userIDFA && value2 == userIDFA_2, "Persisted IDFAs are not the same as User IDFAs or in wrong order") + } else { + XCTFail("Persisted IDFAs are not the same as User IDFAs or in wrong order") + } + } else { + XCTFail("User IDFAs are not persisted") + } + } + + func test_idfa_detection_enabled_enrich_system_idfa_zero_uuid_no_prepend() { + ATT.advertisingIdentifierAvailable_DebugOverride = true + ATT.advertisingIdentifier_DebugOverride = UUID(uuid: uuid_t(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) + + var identifiers: [OptableIdentifier] = [ + .emailAddress("test@test.com"), + .phoneNumber("1234567890"), + ] + + let sdk = buildSDK() + sdk.config.skipAdvertisingIdDetection = false + sdk.enrichIfNeeded(ids: &identifiers) + + if identifiers.contains(where: { if case .appleIDFA = $0 { return true } else { return false } }) { + XCTFail("Zero System IDFA should not be prepended or persisted") + } + } + + // MARK: Builders + + func buildSDK() -> OptableSDK { + return OptableSDK(config: OptableConfig( + tenant: T.api.tenant.prebidtest, + originSlug: T.api.slug.iosSDK, + insecure: false, + customUserAgent: T.api.userAgent, + skipAdvertisingIdDetection: true + )) + } + + func buildIdentifiers() -> [OptableIdentifier] { + [ + .emailAddress("test@test.com"), + .phoneNumber("1234567890"), + .postalCode("12345"), + .ipv4Address("127.0.0.1"), + .ipv6Address("2001:db8::7"), + ] + } +} diff --git a/Tests/Unit/OptableSDKHelpersTests.swift b/Tests/Unit/OptableSDKHelpersTests.swift new file mode 100644 index 0000000..f187141 --- /dev/null +++ b/Tests/Unit/OptableSDKHelpersTests.swift @@ -0,0 +1,151 @@ +// +// OptableSDKHelpersTests.swift +// OptableSDK +// +// Copyright © 2026 Optable Technologies, Inc. All rights reserved. +// + +@testable import OptableSDK +import XCTest + +class OptableSDKHelpersTests: XCTestCase { + // MARK: Identifiers Enrichment + // MARK: GAM Keywords + func test_generateGAMTargetingKeywords_nilOrEmpty() { + XCTAssertNil(OptableSDK.generateGAMTargetingKeywords(from: nil)) + XCTAssertNil(OptableSDK.generateGAMTargetingKeywords(from: [:])) + XCTAssertNil(OptableSDK.generateGAMTargetingKeywords(from: ["user": [:]])) + } + + func test_generateGAMTargetingKeywords_valid() { + let targetingData: NSDictionary = [ + "audience": [ + [ + "keyspace": "ks1", + "ids": [["id": "a1"], ["id": "a2"]], + ], + [ + "keyspace": "ks2", + "ids": [["id": "b1"]], + ], + ], + ] + + let result = OptableSDK.generateGAMTargetingKeywords(from: targetingData) + XCTAssertNotNil(result) + XCTAssertEqual(result?["ks1"] as? String, "a1,a2") + XCTAssertEqual(result?["ks2"] as? String, "b1") + } + + // MARK: ORTB2 Config + func test_generateORTB2Config_nilOrEmpty() { + XCTAssertNil(OptableSDK.generateORTB2Config(from: nil)) + XCTAssertNil(OptableSDK.generateORTB2Config(from: [:])) + XCTAssertNil(OptableSDK.generateORTB2Config(from: ["user": [:]])) + } + + func test_generateORTB2Config_valid() throws { + let ortb2: NSDictionary = [ + "user": [ + "data": [ + [ + "id": "optable.co", + "segment": [["id": "seg-1"], ["id": "seg-2"]], + ], + ], + ], + ] + let targetingData: NSDictionary = [ + "ortb2": ortb2, + ] + + guard let result = OptableSDK.generateORTB2Config(from: targetingData) else { + return XCTFail("Expected non-nil ORTB2 config string") + } + + // Validate by decoding back to JSON and comparing dictionaries + let data = try XCTUnwrap(result.data(using: .utf8)) + let json = try XCTUnwrap(try JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary) + XCTAssertEqual(json, ortb2) + } + + // MARK: OptableTargeting + func test_generateOptableTargeting_nilData_returnsEmpty() throws { + let targeting = try OptableSDK.generateOptableTargeting(from: nil) + XCTAssertEqual(targeting.targetingData, [:]) + XCTAssertNil(targeting.gamTargetingKeywords) + XCTAssertNil(targeting.ortb2) + } + + func test_generateOptableTargeting_parsesAudienceAndORTB2() throws { + let jsonDict: NSDictionary = [ + "audience": [ + [ + "provider": "optable.co", + "keyspace": "ks1", + "ids": [["id": "a1"], ["id": "a2"]], + ], + [ + "provider": "optable.co", + "keyspace": "ks2", + "ids": [["id": "b1"]], + ], + ], + "ortb2": [ + "user": [ + "data": [ + [ + "id": "optable.co", + "segment": [["id": "seg-1"]], + ], + ], + ], + ], + ] + + let data = try JSONSerialization.data(withJSONObject: jsonDict, options: []) + let targeting = try OptableSDK.generateOptableTargeting(from: data) + + // targetingData should reflect input JSON + XCTAssertEqual(targeting.targetingData["audience"] as? NSArray, jsonDict["audience"] as? NSArray) + + // gamTargetingKeywords should be derived from "audience" + XCTAssertEqual(targeting.gamTargetingKeywords?["ks1"] as? String, "a1,a2") + XCTAssertEqual(targeting.gamTargetingKeywords?["ks2"] as? String, "b1") + + // ortb2 should be a JSON string equivalent to provided dict + let ortb2String = try XCTUnwrap(targeting.ortb2) + let ortb2Data = try XCTUnwrap(ortb2String.data(using: .utf8)) + let ortb2Decoded = try XCTUnwrap(try JSONSerialization.jsonObject(with: ortb2Data, options: []) as? NSDictionary) + XCTAssertEqual(ortb2Decoded, jsonDict["ortb2"] as? NSDictionary) + } + + // MARK: EdgeAPIErrorDescription + func test_generateEdgeAPIErrorDescription_includesStatusAndJSON() throws { + let url = try XCTUnwrap(URL(string: "https://example.com")) + let response = try XCTUnwrap(HTTPURLResponse(url: url, statusCode: 418, httpVersion: nil, headerFields: nil)) + let json: NSDictionary = ["error": "I'm a teapot", "code": 418] + let data = try? JSONSerialization.data(withJSONObject: json, options: []) + + let message = OptableSDK.generateEdgeAPIErrorDescription(with: data, response: response) + XCTAssertTrue(message.contains("HTTP response.statusCode: 418")) + XCTAssertTrue(message.contains("error")) + XCTAssertTrue(message.contains("teapot")) + } + + func test_generateEdgeAPIErrorDescription_noData() throws { + let url = try XCTUnwrap(URL(string: "https://example.com")) + let response = try XCTUnwrap(HTTPURLResponse(url: url, statusCode: 500, httpVersion: nil, headerFields: nil)) + + let message = OptableSDK.generateEdgeAPIErrorDescription(with: nil, response: response) + XCTAssertTrue(message.contains("HTTP response.statusCode: 500")) + XCTAssertFalse(message.contains("data:")) + } + + // MARK: Version + func test_version_notUnknown() { + // Should resolve to something like ios-- + XCTAssertNotEqual(OptableSDK.version, "ios-unknown") + XCTAssertTrue(OptableSDK.version.hasPrefix("ios-")) + } +} From 5dd62b11ef82abff249431cdc3a6a44fff46029d Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Tue, 17 Mar 2026 13:58:49 +0100 Subject: [PATCH 4/5] - Updated demo apps host to `na.cloud.optable.co` --- demo-ios-objc/demo-ios-objc/AppDelegate.m | 2 +- demo-ios-swift/demo-ios-swift/AppDelegate.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demo-ios-objc/demo-ios-objc/AppDelegate.m b/demo-ios-objc/demo-ios-objc/AppDelegate.m index c3644d8..14297bc 100644 --- a/demo-ios-objc/demo-ios-objc/AppDelegate.m +++ b/demo-ios-objc/demo-ios-objc/AppDelegate.m @@ -30,7 +30,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( OptableSDKDelegate *delegate = [OptableSDKDelegate new]; OptableConfig *config = [[OptableConfig alloc] initWithTenant: @"prebidtest" originSlug: @"ios-sdk"]; - config.host = @"prebidtest.cloud.optable.co"; + config.host = @"na.cloud.optable.co"; OPTABLE = [[OptableSDK alloc] initWithConfig: config]; OPTABLE.delegate = delegate; diff --git a/demo-ios-swift/demo-ios-swift/AppDelegate.swift b/demo-ios-swift/demo-ios-swift/AppDelegate.swift index 116b410..3584372 100644 --- a/demo-ios-swift/demo-ios-swift/AppDelegate.swift +++ b/demo-ios-swift/demo-ios-swift/AppDelegate.swift @@ -31,7 +31,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let config = OptableConfig( tenant: "prebidtest", originSlug: "ios-sdk", - host: "prebidtest.cloud.optable.co", + host: "na.cloud.optable.co", skipAdvertisingIdDetection: false ) OPTABLE = OptableSDK(config: config) From ea45cd623799b1d07d856ec4c33e626bb1d83fbb Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Fri, 3 Apr 2026 22:47:16 +0200 Subject: [PATCH 5/5] update host to ca.edge.optable.co --- demo-ios-swift/demo-ios-swift/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-ios-swift/demo-ios-swift/AppDelegate.swift b/demo-ios-swift/demo-ios-swift/AppDelegate.swift index 3584372..5dbaabc 100644 --- a/demo-ios-swift/demo-ios-swift/AppDelegate.swift +++ b/demo-ios-swift/demo-ios-swift/AppDelegate.swift @@ -31,7 +31,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let config = OptableConfig( tenant: "prebidtest", originSlug: "ios-sdk", - host: "na.cloud.optable.co", + host: "ca.edge.optable.co", skipAdvertisingIdDetection: false ) OPTABLE = OptableSDK(config: config)