diff --git a/OptableSDK.xcodeproj/project.pbxproj b/OptableSDK.xcodeproj/project.pbxproj index a05613f..fab643e 100644 --- a/OptableSDK.xcodeproj/project.pbxproj +++ b/OptableSDK.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ Unit/OptableIdentifiersTests.swift, Unit/OptableSDKHelpersIdentifiersEnrichmentTests.swift, Unit/OptableSDKHelpersTests.swift, + Unit/OptableTargetingTests.swift, ); target = 6352AB0324EAD403002E66EB /* OptableSDKTests */; }; diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index 893c4ba..0cf63ab 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -63,12 +63,35 @@ final class EdgeAPI { return request } - func targeting(ids: [OptableIdentifier]) throws -> URLRequest? { + func targeting(ids: [OptableIdentifier], hids: [OptableIdentifier]) throws -> URLRequest? { guard var url = buildEdgeAPIURL(endpoint: "targeting") else { return nil } - let queryItems = ids + var queryItems = ids .compactMap({ $0.extendedIdentifier }) .compactMap({ URLQueryItem(name: "id", value: $0) }) + + let hidQueryItems = hids.hids + .compactMap({ $0.extendedIdentifier }) + .compactMap({ URLQueryItem(name: "hid", value: $0) }) + + queryItems.append(contentsOf: hidQueryItems) + + if let bundle = Bundle.main.bundleIdentifier { + queryItems.append(URLQueryItem(name: "bundle", value: bundle)) + } + + if let ver = Bundle.main.appVersionString { + queryItems.append(URLQueryItem(name: "ver", value: ver)) + } + + if let userAgent { + queryItems.append(URLQueryItem(name: "ua", value: userAgent)) + } + + if let id5Signature = storage.getID5Signature() { + queryItems.append(URLQueryItem(name: "id5_signature", value: id5Signature)) + } + url.compatAppend(queryItems: queryItems) let request = try buildRequest(.GET, url: url, headers: resolveHeaders()) diff --git a/Source/Core/LocalStorage.swift b/Source/Core/LocalStorage.swift index 84ff17f..0fe1df0 100644 --- a/Source/Core/LocalStorage.swift +++ b/Source/Core/LocalStorage.swift @@ -15,6 +15,7 @@ final class LocalStorage: NSObject { private let targetingDataKey: String private let gamTargetingKeywordsKey: String private let ortb2Key: String + private let id5SignatureKey: String let keyPfx: String = "OPTABLE" var passportKey: String @@ -33,6 +34,7 @@ final class LocalStorage: NSObject { self.targetingDataKey = targetingKey + "_targetingData" self.gamTargetingKeywordsKey = targetingKey + "_gamTargetingKeywords" self.ortb2Key = targetingKey + "_ortb2" + self.id5SignatureKey = targetingKey + "_id5Signature" } func getPassport() -> String? { @@ -68,5 +70,14 @@ final class LocalStorage: NSObject { UserDefaults.standard.removeObject(forKey: targetingDataKey) UserDefaults.standard.removeObject(forKey: gamTargetingKeywordsKey) UserDefaults.standard.removeObject(forKey: ortb2Key) + UserDefaults.standard.removeObject(forKey: id5SignatureKey) + } + + func getID5Signature() -> String? { + return UserDefaults.standard.string(forKey: id5SignatureKey) + } + + func setID5Signature(_ signature: String) { + UserDefaults.standard.set(signature, forKey: id5SignatureKey) } } diff --git a/Source/Misc/Bundle++.swift b/Source/Misc/Bundle++.swift new file mode 100644 index 0000000..60d23f1 --- /dev/null +++ b/Source/Misc/Bundle++.swift @@ -0,0 +1,15 @@ +// +// Bundle++.swift +// OptableSDK +// +// Copyright © 2026 Optable Technologies, Inc. All rights reserved. +// + +import Foundation + +extension Bundle { + /// The app's release version (`CFBundleShortVersionString`), if present. + var appVersionString: String? { + infoDictionary?["CFBundleShortVersionString"] as? String + } +} diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index 5ea6aeb..f53af85 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -116,18 +116,31 @@ public extension OptableSDK { // MARK: - Targeting public extension OptableSDK { /** - targeting(ids?, completion) calls the Optable Sandbox Targeting API and returns key-value targeting data - for the current user/device/app. You may optionally supply identifiers to enrich the request. - - On completion, the handler receives: - - .success(OptableTargeting) on success - - .failure(Error) on failure + targeting(ids?, hids?, completion) calls the Optable Sandbox Targeting API and returns key-value targeting data + for the current user/device/app. + + - Parameters: + - ids: one or more identifiers to resolve, sent as repeated `id` query parameters. The DCN evaluates them + in the order they are listed and returns the profile of the first successful match (querying the + first-party graph before any third-party graphs). When provided, they take precedence over any + identifier in the passport. + - hids: hint identifiers, sent as `hid` query parameters in addition to `ids`. Hints drive resolver-specific + identity resolution on the DCN, such as ID5 Mobile In-App. Only the identifier types valid as hints are + forwarded: email address, phone number, IPv6 address, Apple IDFA, Google GAID and custom IDs — any other + type in `hids` is dropped client-side. Custom (`cN`) prefixes must be configured on the DCN; unconfigured + ones are ignored server-side. + - completion: on completion, the handler receives: + - .success(OptableTargeting) on success + - .failure(Error) on failure + + Unless `skipAdvertisingIdDetection` is set in the config, the device IDFA is automatically prepended to both + lists when ad tracking is authorized. On success, the result is cached in client storage. You can read it using targetingFromCache() and clear it using targetingClearCache(). */ - func targeting(_ ids: [OptableIdentifier]? = nil, completion: @escaping (Result) -> Void) throws { - try _targeting(ids: ids, completion: completion) + func targeting(_ ids: [OptableIdentifier]? = nil, hids: [OptableIdentifier]? = nil, completion: @escaping (Result) -> Void) throws { + try _targeting(ids: ids, hids: hids, completion: completion) } /// targetingFromCache() returns the previously cached targeting data, if any. @@ -144,15 +157,18 @@ public extension OptableSDK { // MARK: Async/Await support /** - This is the Swift Concurrency compatible version of the `targeting(completion)` API. + This is the Swift Concurrency compatible version of the `targeting(ids, hids, completion)` API: + `ids` match the user/device against the DCN, while `hids` are hint identifiers driving resolver-specific + identity resolution such as ID5 Mobile In-App — see `targeting(_:hids:completion:)` for details on which + identifier types are valid hints. Instead of completion callbacks, results are returned via async/await. */ @available(iOS 13.0, *) - func targeting(_ ids: [OptableIdentifier]? = nil) async throws -> OptableTargeting { + func targeting(_ ids: [OptableIdentifier]? = nil, hids: [OptableIdentifier]? = nil) async throws -> OptableTargeting { return try await withCheckedThrowingContinuation({ [unowned self] continuation in do { - try self._targeting(ids: ids, completion: { continuation.resume(with: $0) }) + try self._targeting(ids: ids, hids: hids, completion: { continuation.resume(with: $0) }) } catch { continuation.resume(throwing: error) } @@ -311,12 +327,14 @@ extension OptableSDK { }).resume() } - func _targeting(ids: [OptableIdentifier]?, completion: @escaping (Result) -> Void) throws { + func _targeting(ids: [OptableIdentifier]?, hids: [OptableIdentifier]?, completion: @escaping (Result) -> Void) throws { var ids = ids ?? [] + var hids = hids ?? [] enrichIfNeeded(ids: &ids) + enrichIfNeeded(ids: &hids) - guard let request = try api.targeting(ids: ids) else { + guard let request = try api.targeting(ids: ids, hids: hids) else { throw OptableError.targeting("Failed to create targeting request") } @@ -340,6 +358,10 @@ extension OptableSDK { /// We cache the latest targeting result in client storage for targetingFromCache() users: self.api.storage.setTargeting(optableTargeting) + + if let id5Signature = optableTargeting.id5Signature { + self.api.storage.setID5Signature(id5Signature) + } completion(.success(optableTargeting)) } catch { diff --git a/Source/Public/ObjCSupport/OptableSDK+ObjC.swift b/Source/Public/ObjCSupport/OptableSDK+ObjC.swift index 5665f35..93e779a 100644 --- a/Source/Public/ObjCSupport/OptableSDK+ObjC.swift +++ b/Source/Public/ObjCSupport/OptableSDK+ObjC.swift @@ -30,14 +30,30 @@ public extension OptableSDK { } /** - This is the Objective-C compatible version of the `targeting(completion)` API. + This is the Objective-C compatible version of the `targeting(ids, completion)` API. Instead of completion callbacks, delegate methods are called. */ @objc func targeting(_ ids: [OptableSDKIdentifier]) throws { + try targeting(ids, hids: []) + } + + /** + This is the Objective-C compatible version of the `targeting(ids, hids, completion)` API: + `ids` match the user/device against the DCN, while `hids` are hint identifiers driving resolver-specific + identity resolution such as ID5 Mobile In-App. Only email address, phone number, IPv6 address, Apple IDFA, + Google GAID and custom identifiers are valid hints — any other type in `hids` is dropped client-side, and + custom (`cN`) prefixes not configured on the DCN are ignored server-side. + + Instead of completion callbacks, delegate methods are called. + */ + @objc(targetingWithIds:hids:error:) + func targeting(_ ids: [OptableSDKIdentifier], hids: [OptableSDKIdentifier]) throws { let bridgedIds = ids.compactMap({ OptableIdentifier(objc: $0) }) - try self._targeting(ids: bridgedIds, completion: { result in + let bridgedHIds = hids.compactMap({ OptableIdentifier(objc: $0) }) + + try self._targeting(ids: bridgedIds, hids: bridgedHIds, completion: { result in switch result { case let .success(optableTargeting): self.delegate?.targetingOk(optableTargeting) diff --git a/Source/Public/OptableIdentifier.swift b/Source/Public/OptableIdentifier.swift index 3152a4e..6fa1e47 100644 --- a/Source/Public/OptableIdentifier.swift +++ b/Source/Public/OptableIdentifier.swift @@ -126,3 +126,22 @@ public extension OptableIdentifier { } } } + +// MARK: - HIDs +extension Array where Element == OptableIdentifier { + /// https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide/resolver-specific-parameters#id5-mobile-in-app + /// + /// Note: all `.custom` identifiers are passed through, but only the custom (`cN`) prefixes + /// configured on the DCN are valid resolver hints — unconfigured ones are ignored server-side. + var hids: [OptableIdentifier] { + filter { + switch $0 { + case .ipv6Address(_), .emailAddress(_), .phoneNumber(_), + .appleIDFA(_), .googleGAID(_), .custom(_, _): + return true + default: + return false + } + } + } +} diff --git a/Source/Public/OptableTargeting.swift b/Source/Public/OptableTargeting.swift index 5710fff..f342c6c 100644 --- a/Source/Public/OptableTargeting.swift +++ b/Source/Public/OptableTargeting.swift @@ -32,3 +32,43 @@ public class OptableTargeting: NSObject { return desc } } + +// MARK: - Helpers + +extension OptableTargeting { + + var id5Signature: String? { + guard let ortb2 = targetingData["ortb2"] as? [String: Any], + let user = ortb2["user"] as? [String: Any], + let eids = user["eids"] as? [[String: Any]] else { + return nil + } + + guard let rootRefs = targetingData["refs"] as? [String: Any] else { return nil } + + for eid in eids { + guard let source = eid["source"] as? String, source.range(of: "id5", options: [.caseInsensitive]) != nil else { + continue + } + + guard let uids = eid["uids"] as? [[String: Any]] else { continue } + + for uid in uids { + guard let ext = uid["ext"] as? [String: Any], + let optable = ext["optable"] as? [String: Any], + let uidRef = optable["ref"] as? String else { + continue + } + + guard let ref = rootRefs[uidRef] as? [String: Any] else { continue } + + if let id5Signature = ref["signature"] as? String, + id5Signature.trimmingCharacters(in: .whitespaces).isEmpty == false { + return id5Signature + } + } + } + + return nil + } +} diff --git a/Tests/Integration/OptableSDKTests.swift b/Tests/Integration/OptableSDKTests.swift index ea6c494..6dcb627 100644 --- a/Tests/Integration/OptableSDKTests.swift +++ b/Tests/Integration/OptableSDKTests.swift @@ -73,7 +73,7 @@ class OptableSDKTests: XCTestCase { } func test_target_delegate() throws { - try sdk.targeting([OptableSDKIdentifier(type: .emailAddress, value: "test@test.com", customIdx: nil)]) + try sdk.targeting([OptableSDKIdentifier(type: .emailAddress, value: "test@test.com", customIdx: nil)], hids: [OptableSDKIdentifier(type: .emailAddress, value: "test@test.com", customIdx: nil)]) wait(for: [targetExpectation], timeout: 10) } diff --git a/Tests/Unit/EdgeAPITests.swift b/Tests/Unit/EdgeAPITests.swift index 602ec0a..6d0428c 100644 --- a/Tests/Unit/EdgeAPITests.swift +++ b/Tests/Unit/EdgeAPITests.swift @@ -17,6 +17,11 @@ class EdgeAPITests: XCTestCase { ) lazy var sdk = OptableSDK(config: config) + override func tearDown() { + sdk.api.storage.clearTargeting() + super.tearDown() + } + // MARK: URL-s /** Expected output: @@ -173,18 +178,47 @@ class EdgeAPITests: XCTestCase { For more info check: [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide/optable-real-time-api-endpoints/targeting) */ func test_targeting_request_generation() throws { - let urlRequest = try sdk.api.targeting(ids: [.emailAddress("12345"), .phoneNumber("54321")]) - + sdk.api.storage.setID5Signature("id5-sig-abc123") + + let email: OptableIdentifier = .emailAddress("12345") + let phone: OptableIdentifier = .phoneNumber("54321") + let urlRequest = try sdk.api.targeting(ids: [email, phone], hids: [email, phone]) + // Method XCTAssertEqual(urlRequest?.httpMethod, HTTPMethod.GET.rawValue) // Path let urlComponents = URLComponents(url: urlRequest!.url!, resolvingAgainstBaseURL: false)! XCTAssert(urlComponents.path.contains("targeting")) - - // Query - XCTAssert(urlComponents.queryItems?.contains(where: { $0.name == "id" && $0.value == "e:12345" }) != nil) - XCTAssert(urlComponents.queryItems?.contains(where: { $0.name == "id" && $0.value == "p:54321" }) != nil) + + // Query: every identifier is emitted as an `id` param (email/phone are SHA-256 hashed) + XCTAssertTrue(urlComponents.queryItems?.contains(where: { $0.name == "id" && $0.value == email.extendedIdentifier }) ?? false) + XCTAssertTrue(urlComponents.queryItems?.contains(where: { $0.name == "id" && $0.value == phone.extendedIdentifier }) ?? false) + + // HIDs: email and phone are part of the HID set, so they are also emitted as `hid` params + XCTAssertTrue(urlComponents.queryItems?.contains(where: { $0.name == "hid" && $0.value == email.extendedIdentifier }) ?? false) + XCTAssertTrue(urlComponents.queryItems?.contains(where: { $0.name == "hid" && $0.value == phone.extendedIdentifier }) ?? false) + + // Resolver-specific parameters + XCTAssertEqual(urlComponents.queryItems?.first(where: { $0.name == "ua" })?.value, T.api.userAgent) + XCTAssertEqual(urlComponents.queryItems?.first(where: { $0.name == "id5_signature" })?.value, "id5-sig-abc123") + + if let bundle = Bundle.main.bundleIdentifier { + XCTAssertEqual(urlComponents.queryItems?.first(where: { $0.name == "bundle" })?.value, bundle) + } + + if let ver = Bundle.main.appVersionString { + XCTAssertEqual(urlComponents.queryItems?.first(where: { $0.name == "ver" })?.value, ver) + } + } + + func test_targeting_request_omits_id5_signature_when_no_cached_signature() throws { + sdk.api.storage.setTargeting(OptableTargeting(optableTargeting: ["resolved_ids": ["v:123"]])) + + let urlRequest = try sdk.api.targeting(ids: [.emailAddress("12345")], hids: []) + let urlComponents = URLComponents(url: urlRequest!.url!, resolvingAgainstBaseURL: false)! + + XCTAssertNil(urlComponents.queryItems?.first(where: { $0.name == "id5_signature" })) } /** diff --git a/Tests/Unit/LocalStorageTests.swift b/Tests/Unit/LocalStorageTests.swift index 28b8aa3..a1df6c8 100644 --- a/Tests/Unit/LocalStorageTests.swift +++ b/Tests/Unit/LocalStorageTests.swift @@ -68,6 +68,14 @@ class LocalStorageTests: XCTestCase { XCTAssert(readTargeting!.ortb2 == nil) } + func testID5SignatureStoring() { + localStorage.setID5Signature("id5-sig-abc123") + XCTAssertEqual(localStorage.getID5Signature(), "id5-sig-abc123") + + localStorage.clearTargeting() + XCTAssertNil(localStorage.getID5Signature()) + } + func testClearOptableTargeting() { let optableTargetingFull = OptableTargeting( optableTargeting: kOptableTargeting as! [String : Any], diff --git a/Tests/Unit/OptableIdentifiersTests.swift b/Tests/Unit/OptableIdentifiersTests.swift index 0160427..cf7a880 100644 --- a/Tests/Unit/OptableIdentifiersTests.swift +++ b/Tests/Unit/OptableIdentifiersTests.swift @@ -63,4 +63,57 @@ class OptableIdentifiersTests: XCTestCase { XCTAssert(c_Idx < c2_Idx) XCTAssert(c2_Idx < c1_Idx) } + + // MARK: - hids + func test_hids_keepsOnlyHIDCases() throws { + let hidOnly: [OptableIdentifier] = [ + .emailAddress("foo@bar.com"), + .phoneNumber("+15123465890"), + .ipv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + .appleIDFA("496f5db5-681f-4392-acd5-0d4f6e2f6b88"), + .googleGAID("64873d9f-d5af-4770-8bcb-167a220eb17d"), + .custom(nil, "d29c551097b9dd0b82423827f65161232efaf7fc"), + .custom(1, "AaaZza.dh012"), + ] + + // Every element is a HID case, so contents and order are preserved. + XCTAssertEqual(hidOnly.hids.map(\.extendedIdentifier), hidOnly.map(\.extendedIdentifier)) + } + + func test_hids_dropsNonHIDCases() throws { + let nonHIDs: [OptableIdentifier] = [ + .postalCode("M5V 3L9"), + .ipv4Address("8.8.8.8"), + .rokuRIDA("0b179df0-6cd5-49f1-be21-425d002e0d22"), + .samsungTIFA("e0ef86a8-6ebf-4c9d-9127-e69407fe748d"), + .amazonFireAFAI("6e853799-ef31-4a30-8706-9742be254d38"), + .netID("_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ"), + .id5("ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg"), + .utiq("496f5db5-681f-4392-acd5-0d4f6e2f6b88"), + .optableVID("v-value"), + ] + + XCTAssertTrue(nonHIDs.hids.isEmpty) + } + + func test_hids_filtersMixedArrayPreservingOrder() throws { + let mixed: [OptableIdentifier] = [ + .postalCode("M5V 3L9"), + .emailAddress("foo@bar.com"), + .ipv4Address("8.8.8.8"), + .appleIDFA("496f5db5-681f-4392-acd5-0d4f6e2f6b88"), + .utiq("496f5db5-681f-4392-acd5-0d4f6e2f6b88"), + .ipv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + .custom(2, "ppid"), + ] + + let expected: [OptableIdentifier] = [ + .emailAddress("foo@bar.com"), + .appleIDFA("496f5db5-681f-4392-acd5-0d4f6e2f6b88"), + .ipv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + .custom(2, "ppid"), + ] + + XCTAssertEqual(mixed.hids.map(\.extendedIdentifier), expected.map(\.extendedIdentifier)) + } } diff --git a/Tests/Unit/OptableTargetingTests.swift b/Tests/Unit/OptableTargetingTests.swift new file mode 100644 index 0000000..ebc274b --- /dev/null +++ b/Tests/Unit/OptableTargetingTests.swift @@ -0,0 +1,86 @@ +// +// OptableTargetingTests.swift +// OptableSDK +// +// Copyright © 2026 Optable Technologies, Inc. All rights reserved. +// + +@testable import OptableSDK +import XCTest + +class OptableTargetingTests: XCTestCase { + /// Targeting data in the shape returned by the edge targeting endpoint: + /// `ortb2.user.eids[].uids[].ext.optable.ref` points into `refs`, where the signature lives. + private func targetingData(source: String = "id5-sync.com", signature: Any = "id5-sig-abc123") -> [String: Any] { + return [ + "ortb2": [ + "user": [ + "eids": [ + [ + "source": source, + "uids": [ + ["id": "ID5*uid", "ext": ["optable": ["ref": "0"]]], + ], + ], + ], + ], + ], + "refs": ["0": ["signature": signature]], + ] + } + + func test_id5Signature_extracted_from_valid_targeting_data() { + let targeting = OptableTargeting(optableTargeting: targetingData()) + XCTAssertEqual(targeting.id5Signature, "id5-sig-abc123") + } + + func test_id5Signature_source_matching_is_case_insensitive() { + let targeting = OptableTargeting(optableTargeting: targetingData(source: "ID5-Sync.com")) + XCTAssertEqual(targeting.id5Signature, "id5-sig-abc123") + } + + func test_id5Signature_nil_when_source_is_not_id5() { + let targeting = OptableTargeting(optableTargeting: targetingData(source: "liveramp.com")) + XCTAssertNil(targeting.id5Signature) + } + + func test_id5Signature_nil_when_signature_empty_or_whitespace() { + XCTAssertNil(OptableTargeting(optableTargeting: targetingData(signature: "")).id5Signature) + XCTAssertNil(OptableTargeting(optableTargeting: targetingData(signature: " ")).id5Signature) + } + + func test_id5Signature_nil_when_signature_is_not_a_string() { + let targeting = OptableTargeting(optableTargeting: targetingData(signature: 123)) + XCTAssertNil(targeting.id5Signature) + } + + func test_id5Signature_nil_when_targeting_data_empty() { + XCTAssertNil(OptableTargeting(optableTargeting: [:]).id5Signature) + } + + func test_id5Signature_nil_when_refs_missing() { + var data = targetingData() + data["refs"] = nil + XCTAssertNil(OptableTargeting(optableTargeting: data).id5Signature) + } + + func test_id5Signature_nil_when_ref_not_found_in_refs() { + var data = targetingData() + data["refs"] = ["other-ref": ["signature": "id5-sig-abc123"]] + XCTAssertNil(OptableTargeting(optableTargeting: data).id5Signature) + } + + func test_id5Signature_nil_when_uid_has_no_optable_ref() { + var data = targetingData() + data["ortb2"] = ["user": ["eids": [["source": "id5-sync.com", "uids": [["id": "ID5*uid"]]]]]] + XCTAssertNil(OptableTargeting(optableTargeting: data).id5Signature) + } + + func test_id5Signature_found_among_multiple_eids() { + var data = targetingData() + var eids = ((data["ortb2"] as! [String: Any])["user"] as! [String: Any])["eids"] as! [[String: Any]] + eids.insert(["source": "liveramp.com", "uids": [["id": "ramp-uid"]]], at: 0) + data["ortb2"] = ["user": ["eids": eids]] + XCTAssertEqual(OptableTargeting(optableTargeting: data).id5Signature, "id5-sig-abc123") + } +} diff --git a/docs/usage-objc.md b/docs/usage-objc.md index 5ed211b..3a1d43a 100644 --- a/docs/usage-objc.md +++ b/docs/usage-objc.md @@ -117,10 +117,27 @@ To get the targeting key values associated by the configured DCN with the device @import OptableSDK; ... NSError *error = nil; -[OPTABLE targetingWithIds: @[@"c:1"] // NULL-able +[OPTABLE targeting: @[ + [OptableSDKIdentifier identifierWithType:OptableSDKIdentifierType_EmailAddress value:@"test@test.test"] +] + error: &error]; +``` + +You may optionally supply hint identifiers (`hids`) which are forwarded as resolver-specific `hid` parameters, used by integrations such as ID5 Mobile In-App: + +```objective-c +[OPTABLE targetingWithIds: @[ + [OptableSDKIdentifier identifierWithType:OptableSDKIdentifierType_EmailAddress value:@"test@test.test"] +] + hids: @[ + [OptableSDKIdentifier identifierWithType:OptableSDKIdentifierType_PhoneNumber value:@"+1234567890"] +] error: &error]; ``` +> :information_source: For more details on `hid` parameters, including the supported identifier types, check: +> [Optable Real-Time API Integrations Guide > Resolver Specific Parameters > ID5 Mobile In-App](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide/resolver-specific-parameters#id5-mobile-in-app) + #### Caching Targeting Data The `targetingAndReturnError` method will automatically cache resulting key value data in client storage on success. You can subsequently retrieve the cached key value data as follows: diff --git a/docs/usage-swift.md b/docs/usage-swift.md index 715e10f..d93364a 100644 --- a/docs/usage-swift.md +++ b/docs/usage-swift.md @@ -138,6 +138,24 @@ do { On success, the resulting key values are typically sent as part of a subsequent ad call. Therefore we recommend that you either call `targeting()` before each ad call, or in parallel periodically, caching the resulting key values which you then provide in ad calls. +You may optionally supply identifiers to enrich the targeting request: `ids` are used to match the user, while `hids` are hint identifiers forwarded as resolver-specific `hid` parameters, used by integrations such as ID5 Mobile In-App: + +```swift +let ids: [OptableIdentifier] = [ + .emailAddress("test@test.test") +] +let hids: [OptableIdentifier] = [ + .phoneNumber("+1234567890") +] + +try OPTABLE!.targeting(ids, hids: hids) { result in + // ... +} +``` + +> :information_source: For more details on `hid` parameters, including the supported identifier types, check: +> [Optable Real-Time API Integrations Guide > Resolver Specific Parameters > ID5 Mobile In-App](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide/resolver-specific-parameters#id5-mobile-in-app) + #### Caching Targeting Data The `targeting` API will automatically cache resulting key value data in client storage on success. You can subsequently retrieve the cached key value data as follows: