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..4396eff 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 + .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/Misc/URL+Compat.swift b/Source/Misc/URL+Compat.swift index c69720a..f22bbbf 100644 --- a/Source/Misc/URL+Compat.swift +++ b/Source/Misc/URL+Compat.swift @@ -9,13 +9,12 @@ import Foundation extension URL { mutating func compatAppend(queryItems: [URLQueryItem]) { - if #available(iOS 16.0, *) { - append(queryItems: queryItems) - } else { - guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { return } - components.queryItems?.append(contentsOf: queryItems) - guard let url = components.url else { return } - self = url - } + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { return } + components.queryItems = (components.queryItems ?? []) + queryItems + // URLComponents leaves `+` literal in the query, but the edge decodes `+` as a space, + // which corrupts base64 values such as the ID5 signature. Encode it explicitly. + components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B") + guard let url = components.url else { return } + self = url } } diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index 5ea6aeb..88588eb 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -116,18 +116,30 @@ 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. All identifier types are forwarded as-is; + which ones a resolver consumes is determined server-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 +156,17 @@ 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. 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 +325,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 +356,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..14ca848 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. All identifier types are forwarded as-is; which ones a + resolver consumes is determined server-side. 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/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..d3fb4b8 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,65 @@ 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 utiq: OptableIdentifier = .utiq("496f5db5-681f-4392-acd5-0d4f6e2f6b88") + let urlRequest = try sdk.api.targeting(ids: [email, phone], hids: [email, phone, utiq]) + // 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: every identifier passed as a hint is emitted as a repeated `hid` param, regardless of type + 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) + XCTAssertTrue(urlComponents.queryItems?.contains(where: { $0.name == "hid" && $0.value == utiq.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_percent_encodes_plus_in_query_values() throws { + // The edge decodes a literal `+` in the query as a space, which would corrupt + // base64 values such as the ID5 signature; it must go out as %2B on the wire. + sdk.api.storage.setID5Signature("sig+abc/123=") + + let urlRequest = try sdk.api.targeting(ids: [], hids: []) + let rawQuery = try XCTUnwrap(urlRequest?.url?.query) + + XCTAssertFalse(rawQuery.contains("+")) + XCTAssertTrue(rawQuery.contains("id5_signature=sig%2Babc/123")) + + // The decoded value round-trips unchanged + let urlComponents = URLComponents(url: urlRequest!.url!, resolvingAgainstBaseURL: false)! + XCTAssertEqual(urlComponents.queryItems?.first(where: { $0.name == "id5_signature" })?.value, "sig+abc/123=") + } + + 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..ec79ef2 100644 --- a/Tests/Unit/OptableIdentifiersTests.swift +++ b/Tests/Unit/OptableIdentifiersTests.swift @@ -63,4 +63,5 @@ class OptableIdentifiersTests: XCTestCase { XCTAssert(c_Idx < c2_Idx) XCTAssert(c2_Idx < c1_Idx) } + } 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..248f2c7 100644 --- a/docs/usage-objc.md +++ b/docs/usage-objc.md @@ -73,7 +73,7 @@ OptableSDK *OPTABLE = nil; You can call various SDK APIs on the instance as shown in the examples below. It's also possible to configure multiple instances of `OptableSDK` in order to connect to other (e.g., partner) DCNs and/or reference other configured application slug IDs. Note that the `insecure` flag should always be set to `NO` unless you are testing a local instance of the DCN yourself. -You can disable user agent `WKWebView` based auto-detection and provide your own value by setting the `useragent` parameter to a string value, similar to the Swift example. +You can disable user agent `WKWebView` based auto-detection and provide your own value by setting the `customUserAgent` parameter to a string value, similar to the Swift example. ### Identify API @@ -117,10 +117,40 @@ 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) + +#### Resolver-Specific Parameters + +On every targeting call the SDK automatically attaches the parameters required by resolver-specific integrations such as ID5 Mobile In-App: + +- `bundle`: the application's bundle identifier. +- `ver`: the application's version (`CFBundleShortVersionString`). +- `ua`: the user agent of the device's default browser, detected asynchronously via `WKWebView` at SDK initialization. If a targeting call happens before detection completes, the parameter is omitted; set the `customUserAgent` configuration parameter to guarantee it is always present. +- `id5_signature`: the ID5 signature cached from a previous targeting response, when available. + +When a targeting response contains an ID5 EID, the SDK extracts the associated signature and caches it in client storage. The cached signature persists across app restarts, is sent as `id5_signature` on subsequent targeting calls to improve ID5 resolve rates, and is removed by `targetingClearCache`. + +In addition, unless `skipAdvertisingIdDetection` is set in the configuration, the device IDFA is automatically added to both `ids` and `hids` when ad tracking is authorized by the user. + #### 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..df990dc 100644 --- a/docs/usage-swift.md +++ b/docs/usage-swift.md @@ -42,14 +42,14 @@ OPTABLE = OptableSDK(config: config) Note that production DCNs only listen to TLS traffic. The `insecure: true` option is meant to be used by Optable developers running the DCN locally for testing. -By default, the SDK detects the application user agent by sniffing `navigator.userAgent` from a `WKWebView`. The resulting user agent string is sent to your DCN for analytics purposes. To disable this behavior, you can provide an optional string parameter, `useragent`, which allows you to set whatever user agent string you would like to send instead. For example: +By default, the SDK detects the application user agent by sniffing `navigator.userAgent` from a `WKWebView`. The resulting user agent string is sent to your DCN for analytics purposes. To disable this behavior, you can provide an optional string parameter, `customUserAgent`, which allows you to set whatever user agent string you would like to send instead. For example: ```swift -let config = OptableConfig(..., useragent: "custom-ua") +let config = OptableConfig(..., customUserAgent: "custom-ua") OPTABLE = OptableSDK(config: config) ``` -The default value of `nil` for the `useragent` parameter enables the `WKWebView` auto-detection behavior. +The default value of `nil` for the `customUserAgent` parameter enables the `WKWebView` auto-detection behavior. ### Identify API @@ -138,6 +138,37 @@ 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) + +#### Resolver-Specific Parameters + +On every `targeting()` call the SDK automatically attaches the parameters required by resolver-specific integrations such as ID5 Mobile In-App: + +- `bundle`: the application's bundle identifier. +- `ver`: the application's version (`CFBundleShortVersionString`). +- `ua`: the user agent of the device's default browser, detected asynchronously via `WKWebView` at SDK initialization. If a targeting call happens before detection completes, the parameter is omitted; set `customUserAgent` in `OptableConfig` to guarantee it is always present. +- `id5_signature`: the ID5 signature cached from a previous targeting response, when available. + +When a targeting response contains an ID5 EID, the SDK extracts the associated signature and caches it in client storage. The cached signature persists across app restarts, is sent as `id5_signature` on subsequent targeting calls to improve ID5 resolve rates, and is removed by `targetingClearCache()`. + +In addition, unless `skipAdvertisingIdDetection` is set in `OptableConfig`, the device IDFA is automatically added to both `ids` and `hids` when ad tracking is authorized by the user. + #### 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: