From 03b40d53f2205eba7f0bd4e86d53f9524acfd317 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 15 Dec 2025 16:11:36 +0100 Subject: [PATCH 01/42] - Namespaced Edge API --- OptableSDK.xcodeproj/project.pbxproj | 108 ++++++++------------------- Source/Edge/Edge.swift | 35 +++++++++ Source/Edge/Identify.swift | 15 ---- Source/Edge/Profile.swift | 15 ---- Source/Edge/Targeting.swift | 15 ---- Source/Edge/Witness.swift | 15 ---- Source/OptableSDK.swift | 8 +- 7 files changed, 70 insertions(+), 141 deletions(-) create mode 100644 Source/Edge/Edge.swift delete mode 100644 Source/Edge/Identify.swift delete mode 100644 Source/Edge/Profile.swift delete mode 100644 Source/Edge/Targeting.swift delete mode 100644 Source/Edge/Witness.swift diff --git a/OptableSDK.xcodeproj/project.pbxproj b/OptableSDK.xcodeproj/project.pbxproj index 05287e6..45df223 100644 --- a/OptableSDK.xcodeproj/project.pbxproj +++ b/OptableSDK.xcodeproj/project.pbxproj @@ -3,21 +3,11 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ - 630E7C0E2523FBBD00AD85C0 /* Witness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630E7C0D2523FBBD00AD85C0 /* Witness.swift */; }; - 63517848256CA65200D6932F /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63517847256CA65200D6932F /* Profile.swift */; }; 6352AB0524EAD403002E66EB /* OptableSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6352AAFB24EAD403002E66EB /* OptableSDK.framework */; }; - 6352AB0A24EAD403002E66EB /* OptableSDKTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6352AB0924EAD403002E66EB /* OptableSDKTests.swift */; }; - 6352AB0C24EAD403002E66EB /* OptableSDK.h in Headers */ = {isa = PBXBuildFile; fileRef = 6352AAFE24EAD403002E66EB /* OptableSDK.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 6352AB1624EAD488002E66EB /* OptableSDK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6352AB1524EAD488002E66EB /* OptableSDK.swift */; }; - 6358779024EC666C008EE46B /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6358778F24EC666C008EE46B /* Config.swift */; }; - 6358779924EC68E8008EE46B /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6358779824EC68E8008EE46B /* Client.swift */; }; - 6358779B24EC6A47008EE46B /* LocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6358779A24EC6A47008EE46B /* LocalStorage.swift */; }; - 6358779E24EC6C00008EE46B /* Identify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6358779D24EC6C00008EE46B /* Identify.swift */; }; - 63643F49251D0AFB007BD90F /* Targeting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63643F48251D0AFB007BD90F /* Targeting.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -31,22 +21,35 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 630E7C0D2523FBBD00AD85C0 /* Witness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Witness.swift; sourceTree = ""; }; - 63517847256CA65200D6932F /* Profile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; 6352AAFB24EAD403002E66EB /* OptableSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OptableSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 6352AAFE24EAD403002E66EB /* OptableSDK.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OptableSDK.h; sourceTree = ""; }; - 6352AAFF24EAD403002E66EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 6352AB0424EAD403002E66EB /* OptableSDKTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OptableSDKTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 6352AB0924EAD403002E66EB /* OptableSDKTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptableSDKTests.swift; sourceTree = ""; }; - 6352AB0B24EAD403002E66EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 6352AB1524EAD488002E66EB /* OptableSDK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptableSDK.swift; sourceTree = ""; }; - 6358778F24EC666C008EE46B /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; - 6358779824EC68E8008EE46B /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; - 6358779A24EC6A47008EE46B /* LocalStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalStorage.swift; sourceTree = ""; }; - 6358779D24EC6C00008EE46B /* Identify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identify.swift; sourceTree = ""; }; - 63643F48251D0AFB007BD90F /* Targeting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Targeting.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + CEBE03882EF03AD00027D67F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + publicHeaders = ( + OptableSDK.h, + ); + target = 6352AAFA24EAD403002E66EB /* OptableSDK */; + }; + CEBE038D2EF03ADD0027D67F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + OptableSDKTests.swift, + ); + target = 6352AB0324EAD403002E66EB /* OptableSDKTests */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + CEBE03802EF03AD00027D67F /* Source */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (CEBE03882EF03AD00027D67F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Source; sourceTree = ""; }; + CEBE038B2EF03ADD0027D67F /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (CEBE038D2EF03ADD0027D67F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 6352AAF824EAD403002E66EB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -69,8 +72,8 @@ 6352AAF124EAD402002E66EB = { isa = PBXGroup; children = ( - 6352AAFD24EAD403002E66EB /* Source */, - 6352AB0824EAD403002E66EB /* Tests */, + CEBE03802EF03AD00027D67F /* Source */, + CEBE038B2EF03ADD0027D67F /* Tests */, 6352AAFC24EAD403002E66EB /* Products */, ); sourceTree = ""; @@ -84,48 +87,6 @@ name = Products; sourceTree = ""; }; - 6352AAFD24EAD403002E66EB /* Source */ = { - isa = PBXGroup; - children = ( - 6358779C24EC6BF0008EE46B /* Edge */, - 6358779724EC68A6008EE46B /* Core */, - 6352AB1524EAD488002E66EB /* OptableSDK.swift */, - 6352AAFE24EAD403002E66EB /* OptableSDK.h */, - 6352AAFF24EAD403002E66EB /* Info.plist */, - 6358778F24EC666C008EE46B /* Config.swift */, - ); - path = Source; - sourceTree = ""; - }; - 6352AB0824EAD403002E66EB /* Tests */ = { - isa = PBXGroup; - children = ( - 6352AB0924EAD403002E66EB /* OptableSDKTests.swift */, - 6352AB0B24EAD403002E66EB /* Info.plist */, - ); - path = Tests; - sourceTree = ""; - }; - 6358779724EC68A6008EE46B /* Core */ = { - isa = PBXGroup; - children = ( - 6358779824EC68E8008EE46B /* Client.swift */, - 6358779A24EC6A47008EE46B /* LocalStorage.swift */, - ); - path = Core; - sourceTree = ""; - }; - 6358779C24EC6BF0008EE46B /* Edge */ = { - isa = PBXGroup; - children = ( - 63517847256CA65200D6932F /* Profile.swift */, - 63643F48251D0AFB007BD90F /* Targeting.swift */, - 6358779D24EC6C00008EE46B /* Identify.swift */, - 630E7C0D2523FBBD00AD85C0 /* Witness.swift */, - ); - path = Edge; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -133,7 +94,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 6352AB0C24EAD403002E66EB /* OptableSDK.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -153,6 +113,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + CEBE03802EF03AD00027D67F /* Source */, + ); name = OptableSDK; productName = OptableSDK; productReference = 6352AAFB24EAD403002E66EB /* OptableSDK.framework */; @@ -237,14 +200,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6352AB1624EAD488002E66EB /* OptableSDK.swift in Sources */, - 63517848256CA65200D6932F /* Profile.swift in Sources */, - 63643F49251D0AFB007BD90F /* Targeting.swift in Sources */, - 6358779E24EC6C00008EE46B /* Identify.swift in Sources */, - 630E7C0E2523FBBD00AD85C0 /* Witness.swift in Sources */, - 6358779B24EC6A47008EE46B /* LocalStorage.swift in Sources */, - 6358779024EC666C008EE46B /* Config.swift in Sources */, - 6358779924EC68E8008EE46B /* Client.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -252,7 +207,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6352AB0A24EAD403002E66EB /* OptableSDKTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Source/Edge/Edge.swift b/Source/Edge/Edge.swift new file mode 100644 index 0000000..a00e37d --- /dev/null +++ b/Source/Edge/Edge.swift @@ -0,0 +1,35 @@ +// +// Edge.swift +// OptableSDK +// +// Created by user on 15.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +import Foundation + +enum Edge { + static func profile(config: Config, client: Client, traits: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + guard let url = config.edgeURL("profile") else { return nil } + let req = try client.postRequest(url: url, data: ["traits": traits]) + return client.dispatchRequest(req, completionHandler) + } + + static func targeting(config: Config, client: Client, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + guard let url = config.edgeURL("targeting") else { return nil } + let req = try client.getRequest(url: url) + return client.dispatchRequest(req, completionHandler) + } + + static func identify(config: Config, client: Client, ids: [String], completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + guard let url = config.edgeURL("identify") else { return nil } + let req = try client.postRequest(url: url, data: ids) + return client.dispatchRequest(req, completionHandler) + } + + static func witness(config: Config, client: Client, event: String, properties: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + guard let url = config.edgeURL("witness") else { return nil } + let req = try client.postRequest(url: url, data: ["event": event, "properties": properties]) + return client.dispatchRequest(req, completionHandler) + } +} diff --git a/Source/Edge/Identify.swift b/Source/Edge/Identify.swift deleted file mode 100644 index af4ea58..0000000 --- a/Source/Edge/Identify.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Identify.swift -// OptableSDK -// -// Copyright © 2020 Optable Technologies Inc. All rights reserved. -// See LICENSE for details. -// - -import Foundation - -func Identify(config: Config, client: Client, ids: [String], completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.edgeURL("identify") else { return nil } - let req = try client.postRequest(url: url, data: ids) - return client.dispatchRequest(req, completionHandler) -} diff --git a/Source/Edge/Profile.swift b/Source/Edge/Profile.swift deleted file mode 100644 index c3d5ae6..0000000 --- a/Source/Edge/Profile.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Profile.swift -// OptableSDK -// -// Copyright © 2020 Optable Technologies, Inc. All rights reserved. -// See LICENSE for details. -// - -import Foundation - -func Profile(config: Config, client: Client, traits: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.edgeURL("profile") else { return nil } - let req = try client.postRequest(url: url, data: ["traits": traits]) - return client.dispatchRequest(req, completionHandler) -} diff --git a/Source/Edge/Targeting.swift b/Source/Edge/Targeting.swift deleted file mode 100644 index 12701c1..0000000 --- a/Source/Edge/Targeting.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Targeting.swift -// OptableSDK -// -// Copyright © 2020 Optable Technologies, Inc. All rights reserved. -// See LICENSE for details. -// - -import Foundation - -func Targeting(config: Config, client: Client, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.edgeURL("targeting") else { return nil } - let req = try client.getRequest(url: url) - return client.dispatchRequest(req, completionHandler) -} diff --git a/Source/Edge/Witness.swift b/Source/Edge/Witness.swift deleted file mode 100644 index 1b4d9e3..0000000 --- a/Source/Edge/Witness.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Witness.swift -// OptableSDK -// -// Copyright © 2020 Optable Technologies, Inc. All rights reserved. -// See LICENSE for details. -// - -import Foundation - -func Witness(config: Config, client: Client, event: String, properties: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.edgeURL("witness") else { return nil } - let req = try client.postRequest(url: url, data: ["event": event, "properties": properties]) - return client.dispatchRequest(req, completionHandler) -} diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index be2ec69..cd70ee5 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -78,7 +78,7 @@ public class OptableSDK: NSObject { /// it either the HTTPURLResponse on success, or an OptableError on failure. /// public func identify(ids: [String], _ completion: @escaping (Result) -> Void) throws -> Void { - try Identify(config: self.config, client: self.client, ids: ids) { (data, response, error) in + try Edge.identify(config: self.config, client: self.client, ids: ids) { (data, response, error) in guard let response = response as? HTTPURLResponse, error == nil, data != nil else { if let err = error { completion(.failure(OptableError.identify("Session error: \(err)"))) @@ -183,7 +183,7 @@ public class OptableSDK: NSObject { /// be access using targetingFromCache(), and cleared using targetingClearCache(). /// public func targeting(_ completion: @escaping (Result) -> Void) throws -> Void { - try Targeting(config: self.config, client: self.client) { (data, response, error) in + try Edge.targeting(config: self.config, client: self.client) { (data, response, error) in guard let response = response as? HTTPURLResponse, error == nil, data != nil else { if let err = error { completion(.failure(OptableError.targeting("Session error: \(err)"))) @@ -262,7 +262,7 @@ public class OptableSDK: NSObject { /// passing it either the HTTPURLResponse on success, or an OptableError on failure. /// public func witness(event: String, properties: NSDictionary, _ completion: @escaping (Result) -> Void) throws -> Void { - try Witness(config: self.config, client: self.client, event: event, properties: properties) { (data, response, error) in + try Edge.witness(config: self.config, client: self.client, event: event, properties: properties) { (data, response, error) in guard let response = response as? HTTPURLResponse, error == nil else { if let err = error { completion(.failure(OptableError.witness("Session error: \(err)"))) @@ -311,7 +311,7 @@ public class OptableSDK: NSObject { /// passing it either the HTTPURLResponse on success, or an OptableError on failure. /// public func profile(traits: NSDictionary, _ completion: @escaping (Result) -> Void) throws -> Void { - try Profile(config: self.config, client: self.client, traits: traits) { (data, response, error) in + try Edge.profile(config: self.config, client: self.client, traits: traits) { (data, response, error) in guard let response = response as? HTTPURLResponse, error == nil else { if let err = error { completion(.failure(OptableError.profile("Session error: \(err)"))) From 4aa130d76a4396c40dee13903154887a37453f37 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 15 Dec 2025 16:23:23 +0100 Subject: [PATCH 02/42] - Updated OptableSDK.version generation --- Source/OptableSDK.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index cd70ee5..db52e28 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -461,9 +461,13 @@ public class OptableSDK: NSObject { /// Cocoapods, it will be set automatically on `pod install` according to the podspec version. /// public static var version: String { - guard let version = Bundle(for: OptableSDK.self).infoDictionary?["CFBundleShortVersionString"] as? String else { - return "ios-unknown" - } - return "ios-" + version + let sdkBundle = Bundle(for: OptableSDK.self) + + guard + let marketingVersion = sdkBundle.infoDictionary?["CFBundleShortVersionString"] as? String, + let buildNumber = sdkBundle.infoDictionary?["CFBundleVersion"] as? String + else { return "ios-unknown" } + + return ["ios", marketingVersion, buildNumber].joined(separator: "-") } } From 16eb7aeee10bb38f59b961add3b5229831d00de6 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 15 Dec 2025 16:55:26 +0100 Subject: [PATCH 03/42] - Introduced public `OptableConfig` instead of internal `Config` --- Source/Config.swift | 27 ------------ Source/Core/Client.swift | 6 +-- Source/Core/LocalStorage.swift | 17 +++++--- Source/Edge/Edge.swift | 8 ++-- Source/OptableConfig.swift | 69 +++++++++++++++++++++++++++++ Source/OptableSDK.swift | 80 +++++++++++++++++----------------- 6 files changed, 127 insertions(+), 80 deletions(-) delete mode 100644 Source/Config.swift create mode 100644 Source/OptableConfig.swift diff --git a/Source/Config.swift b/Source/Config.swift deleted file mode 100644 index 7be2870..0000000 --- a/Source/Config.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Config.swift -// OptableSDK -// -// Copyright © 2020 Optable Technologies Inc. All rights reserved. -// See LICENSE for details. -// - -import Foundation - -struct Config { - var host: String - var app: String - var insecure: Bool - var useragent: String? - - func edgeURL(_ path: String) -> URL? { - var proto = "https://" - if self.insecure { - proto = "http://" - } - - guard var components = URLComponents(string: proto + self.host + "/" + self.app + "/" + path) else { return nil } - components.queryItems = [ URLQueryItem(name: "osdk", value: OptableSDK.version) ] - return components.url - } -} diff --git a/Source/Core/Client.swift b/Source/Core/Client.swift index 358c7f4..17bf41e 100644 --- a/Source/Core/Client.swift +++ b/Source/Core/Client.swift @@ -14,14 +14,14 @@ class Client { var storage: LocalStorage var ua: String? - init(_ config: Config) { + init(_ config: OptableConfig) { self.storage = LocalStorage(config) - if (config.useragent == nil) { + if (config.customUserAgent == nil) { self.userAgent { (realUserAgent) in self.ua = realUserAgent } } else { - self.ua = config.useragent + self.ua = config.customUserAgent } } diff --git a/Source/Core/LocalStorage.swift b/Source/Core/LocalStorage.swift index 6ae2729..012c8bb 100644 --- a/Source/Core/LocalStorage.swift +++ b/Source/Core/LocalStorage.swift @@ -16,19 +16,22 @@ class LocalStorage: NSObject { var passportKey: String var targetingKey: String - init(_ config: Config) { + init(_ config: OptableConfig) { // The key used for storage should be unique to the host+app that this instance was initialized with: - let utf8str = (config.host + "/" + config.app).data(using: .utf8)?.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)) + let base64Key: String? = [config.host, config.tenant, config.originSlug] + .joined(separator: "/") + .data(using: .utf8)? + .base64EncodedString() - self.passportKey = self.keyPfx + "_PASS_" + (utf8str ?? "UNKNOWN") - self.targetingKey = self.keyPfx + "_TGT_" + (utf8str ?? "UNKNOWN") + self.passportKey = self.keyPfx + "_PASS_" + (base64Key ?? "UNKNOWN") + self.targetingKey = self.keyPfx + "_TGT_" + (base64Key ?? "UNKNOWN") } func getPassport() -> String? { return UserDefaults.standard.string(forKey: passportKey) } - func setPassport(_ passport: String) -> Void { + func setPassport(_ passport: String) { UserDefaults.standard.set(passport, forKey: passportKey) } @@ -36,11 +39,11 @@ class LocalStorage: NSObject { return UserDefaults.standard.dictionary(forKey: targetingKey) } - func setTargeting(_ keyvalues: [String: Any]) -> Void { + func setTargeting(_ keyvalues: [String: Any]) { UserDefaults.standard.setValue(keyvalues, forKey: targetingKey) } - func clearTargeting() -> Void { + func clearTargeting() { UserDefaults.standard.removeObject(forKey: targetingKey) } } diff --git a/Source/Edge/Edge.swift b/Source/Edge/Edge.swift index a00e37d..e14e29c 100644 --- a/Source/Edge/Edge.swift +++ b/Source/Edge/Edge.swift @@ -9,25 +9,25 @@ import Foundation enum Edge { - static func profile(config: Config, client: Client, traits: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + static func profile(config: OptableConfig, client: Client, traits: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { guard let url = config.edgeURL("profile") else { return nil } let req = try client.postRequest(url: url, data: ["traits": traits]) return client.dispatchRequest(req, completionHandler) } - static func targeting(config: Config, client: Client, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + static func targeting(config: OptableConfig, client: Client, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { guard let url = config.edgeURL("targeting") else { return nil } let req = try client.getRequest(url: url) return client.dispatchRequest(req, completionHandler) } - static func identify(config: Config, client: Client, ids: [String], completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + static func identify(config: OptableConfig, client: Client, ids: [String], completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { guard let url = config.edgeURL("identify") else { return nil } let req = try client.postRequest(url: url, data: ids) return client.dispatchRequest(req, completionHandler) } - static func witness(config: Config, client: Client, event: String, properties: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + static func witness(config: OptableConfig, client: Client, event: String, properties: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { guard let url = config.edgeURL("witness") else { return nil } let req = try client.postRequest(url: url, data: ["event": event, "properties": properties]) return client.dispatchRequest(req, completionHandler) diff --git a/Source/OptableConfig.swift b/Source/OptableConfig.swift new file mode 100644 index 0000000..f130fa1 --- /dev/null +++ b/Source/OptableConfig.swift @@ -0,0 +1,69 @@ +// +// OptableConfig.swift +// OptableSDK +// +// Copyright © 2020 Optable Technologies Inc. All rights reserved. +// See LICENSE for details. +// + +import Foundation + +@objc +public class OptableConfig: NSObject { + /// The tenant name associated with the configuration. E.g. `acmeco.optable.co` => `acmeco`. + public var tenant: String + + /// The DCN's Source Slug. E.g. `acmeco-sdk`. + public var originSlug: String + + /// The hostname of the Optable endpoint. Default value is "na.edge.optable.co". + public var host: String + + /// The API path to be appended to the host. Default value is "v2". + public var path: String + + /// Boolean flag that determines if insecure HTTP should be used instead of HTTPS. Default is false. + public var insecure: Bool + + /// An optional API key for authentication. If the API Endpoint is enabled as private, a Service Account API key will be required. + public var apiKey: String? + + /// An optional custom user agent string for network requests. + public var customUserAgent: String? + + /// Boolean flag to skip the detection of advertising IDs. Default is false. + public var skipAdvertisingIdDetection: Bool // FIXME: is really used? + + public init( + tenant: String, + originSlug: String, + host: String = "na.edge.optable.co", + path: String = "v2", + insecure: Bool = false, + apiKey: String? = nil, + customUserAgent: String? = nil, + skipAdvertisingIdDetection: Bool = false + ) { + self.tenant = tenant + self.originSlug = originSlug + self.host = host + self.path = path + self.insecure = insecure + self.apiKey = apiKey + self.customUserAgent = customUserAgent + self.skipAdvertisingIdDetection = skipAdvertisingIdDetection + } + + func edgeURL(_ endpoint: String) -> URL? { + var components = URLComponents() + components.scheme = insecure ? "http" : "https" + components.host = host + components.path = "/\(path)/\(endpoint)" + components.queryItems = [ + .init(name: "t", value: tenant.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), + .init(name: "o", value: originSlug.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), + .init(name: "osdk", value: OptableSDK.version.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), + ] + return components.url + } +} diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index db52e28..5a8d7e2 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -6,14 +6,15 @@ // See LICENSE for details. // -import Foundation import CommonCrypto +import Foundation #if canImport(CryptoKit) -import CryptoKit + import CryptoKit #endif -import AppTrackingTransparency import AdSupport +import AppTrackingTransparency +// MARK: - OptableDelegate /// /// OptableDelegate is a delegate protocol that the caller may optionally use. Swift applications can choose to integrate using /// callbacks or the delegator pattern, whereas Objective-C apps must use the delegator pattern. @@ -39,6 +40,7 @@ public protocol OptableDelegate { func witnessErr(_ error: NSError) } +// MARK: - OptableSDK /// /// OptableSDK exposes an API that is used by an iOS app developer integrating with an Optable Sandbox. /// @@ -60,16 +62,16 @@ public class OptableSDK: NSObject { case witness(String) } - var config: Config + var config: OptableConfig var client: Client /// - /// OptableSDK(host, app) returns an instance of the SDK configured to talk to the sandbox specified by host & app: + /// `OptableSDK` returns an instance of the SDK configured to talk to the sandbox specified by `OptableConfig`: /// @objc - public init(host: String, app: String, insecure: Bool = false, useragent: String? = nil) { - self.config = Config(host: host, app: app, insecure: insecure, useragent: useragent) - self.client = Client(self.config) + public init(config: OptableConfig) { + self.config = config + self.client = Client(config) } /// @@ -77,8 +79,8 @@ public class OptableSDK: NSObject { /// list of type-prefixed IDs. It is asynchronous, and on completion it will call the specified completion handler, passing /// it either the HTTPURLResponse on success, or an OptableError on failure. /// - public func identify(ids: [String], _ completion: @escaping (Result) -> Void) throws -> Void { - try Edge.identify(config: self.config, client: self.client, ids: ids) { (data, response, error) in + public func identify(ids: [String], _ completion: @escaping (Result) -> Void) throws { + try Edge.identify(config: self.config, client: self.client, ids: ids) { data, response, error in guard let response = response as? HTTPURLResponse, error == nil, data != nil else { if let err = error { completion(.failure(OptableError.identify("Session error: \(err)"))) @@ -107,12 +109,12 @@ public class OptableSDK: NSObject { /// This is the Objective-C compatible version of the identify(ids, completion) API. /// @objc - public func identify(_ ids: [String]) throws -> Void { + public func identify(_ ids: [String]) throws { try self.identify(ids: ids) { result in switch result { - case .success(let response): + case let .success(response): self.delegate?.identifyOk(response) - case .failure(let error as NSError): + case let .failure(error as NSError): self.delegate?.identifyErr(error) } } @@ -126,10 +128,10 @@ public class OptableSDK: NSObject { /// The identify method is asynchronous, and on completion it will call the specified completion handler, passing /// it either the HTTPURLResponse on success, or an OptableError on failure. /// - public func identify(email: String, aaid: Bool = false, ppid: String = "", _ completion: @escaping (Result) -> Void) throws -> Void { + public func identify(email: String, aaid: Bool = false, ppid: String = "", _ completion: @escaping (Result) -> Void) throws { var ids = [String]() - if (email != "") { + if email != "" { ids.append(self.eid(email)) } @@ -147,7 +149,7 @@ public class OptableSDK: NSObject { } } - if ppid.count > 0 { + if !ppid.isEmpty { ids.append(self.cid(ppid)) } @@ -161,12 +163,12 @@ public class OptableSDK: NSObject { /// This is the Objective-C compatible version of the identify(email, aaid, ppid, completion) API. /// @objc - public func identify(_ email: String, aaid: Bool = false, ppid: String = "") throws -> Void { + public func identify(_ email: String, aaid: Bool = false, ppid: String = "") throws { try self.identify(email: email, aaid: aaid, ppid: ppid) { result in switch result { - case .success(let response): + case let .success(response): self.delegate?.identifyOk(response) - case .failure(let error as NSError): + case let .failure(error as NSError): self.delegate?.identifyErr(error) } } @@ -182,8 +184,8 @@ public class OptableSDK: NSObject { /// On success, this method will also cache the resulting targeting data in client storage, which can /// be access using targetingFromCache(), and cleared using targetingClearCache(). /// - public func targeting(_ completion: @escaping (Result) -> Void) throws -> Void { - try Edge.targeting(config: self.config, client: self.client) { (data, response, error) in + public func targeting(_ completion: @escaping (Result) -> Void) throws { + try Edge.targeting(config: self.config, client: self.client) { data, response, error in guard let response = response as? HTTPURLResponse, error == nil, data != nil else { if let err = error { completion(.failure(OptableError.targeting("Session error: \(err)"))) @@ -223,12 +225,12 @@ public class OptableSDK: NSObject { /// This is the Objective-C compatible version of the targeting(completion) API. /// @objc - public func targeting() throws -> Void { - try self.targeting() { result in + public func targeting() throws { + try self.targeting { result in switch result { - case .success(let keyvalues): + case let .success(keyvalues): self.delegate?.targetingOk(keyvalues) - case .failure(let error as NSError): + case let .failure(error as NSError): self.delegate?.targetingErr(error) } } @@ -249,7 +251,7 @@ public class OptableSDK: NSObject { /// targetingClearCache() clears any previously cached targeting data. /// @objc - public func targetingClearCache() -> Void { + public func targetingClearCache() { self.client.storage.clearTargeting() } @@ -261,8 +263,8 @@ public class OptableSDK: NSObject { /// The witness method is asynchronous, and on completion it will call the specified completion handler, /// passing it either the HTTPURLResponse on success, or an OptableError on failure. /// - public func witness(event: String, properties: NSDictionary, _ completion: @escaping (Result) -> Void) throws -> Void { - try Edge.witness(config: self.config, client: self.client, event: event, properties: properties) { (data, response, error) in + public func witness(event: String, properties: NSDictionary, _ completion: @escaping (Result) -> Void) throws { + try Edge.witness(config: self.config, client: self.client, event: event, properties: properties) { data, response, error in guard let response = response as? HTTPURLResponse, error == nil else { if let err = error { completion(.failure(OptableError.witness("Session error: \(err)"))) @@ -291,12 +293,12 @@ public class OptableSDK: NSObject { /// This is the Objective-C compatible version of the witness(event, properties, completion) API. /// @objc - public func witness(_ event: String, properties: NSDictionary) throws -> Void { + public func witness(_ event: String, properties: NSDictionary) throws { try self.witness(event: event, properties: properties) { result in switch result { - case .success(let response): + case let .success(response): self.delegate?.witnessOk(response) - case .failure(let error as NSError): + case let .failure(error as NSError): self.delegate?.witnessErr(error) } } @@ -310,8 +312,8 @@ public class OptableSDK: NSObject { /// The profile method is asynchronous, and on completion it will call the specified completion handler, /// passing it either the HTTPURLResponse on success, or an OptableError on failure. /// - public func profile(traits: NSDictionary, _ completion: @escaping (Result) -> Void) throws -> Void { - try Edge.profile(config: self.config, client: self.client, traits: traits) { (data, response, error) in + public func profile(traits: NSDictionary, _ completion: @escaping (Result) -> Void) throws { + try Edge.profile(config: self.config, client: self.client, traits: traits) { data, response, error in guard let response = response as? HTTPURLResponse, error == nil else { if let err = error { completion(.failure(OptableError.profile("Session error: \(err)"))) @@ -340,12 +342,12 @@ public class OptableSDK: NSObject { /// This is the Objective-C compatible version of the profile(traits, completion) API. /// @objc - public func profile(traits: NSDictionary) throws -> Void { + public func profile(traits: NSDictionary) throws { try self.profile(traits: traits) { result in switch result { - case .success(let response): + case let .success(response): self.delegate?.profileOk(response) - case .failure(let error as NSError): + case let .failure(error as NSError): self.delegate?.profileErr(error) } } @@ -447,10 +449,10 @@ public class OptableSDK: NSObject { /// links in newsletter Emails sent by the application developer. /// @objc - public func tryIdentifyFromURL(_ urlString: String) throws -> Void { + public func tryIdentifyFromURL(_ urlString: String) throws { let oeid = self.eidFromURL(urlString) - if (oeid.count > 0) { + if !oeid.isEmpty { try self.identify(ids: [oeid]) { _ in /* no-op */ } } } @@ -462,7 +464,7 @@ public class OptableSDK: NSObject { /// public static var version: String { let sdkBundle = Bundle(for: OptableSDK.self) - + guard let marketingVersion = sdkBundle.infoDictionary?["CFBundleShortVersionString"] as? String, let buildNumber = sdkBundle.infoDictionary?["CFBundleVersion"] as? String From 9706854ba7536224e9eba5b5f8ac7cf3eaab57f2 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 15 Dec 2025 18:30:42 +0100 Subject: [PATCH 04/42] - Refactored Networking & API layers --- Source/Core/Client.swift | 124 ---------------- Source/Core/EdgeAPI.swift | 150 ++++++++++++++++++++ Source/Core/LocalStorage.swift | 2 +- Source/Core/Networking.swift | 223 +++++++++++++++++++++++++++++ Source/Edge/Edge.swift | 35 ----- Source/OptableSDK.swift | 251 +++++++++++++++++---------------- 6 files changed, 504 insertions(+), 281 deletions(-) delete mode 100644 Source/Core/Client.swift create mode 100644 Source/Core/EdgeAPI.swift create mode 100644 Source/Core/Networking.swift delete mode 100644 Source/Edge/Edge.swift diff --git a/Source/Core/Client.swift b/Source/Core/Client.swift deleted file mode 100644 index 17bf41e..0000000 --- a/Source/Core/Client.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// Client.swift -// OptableSDK -// -// Copyright © 2020 Optable Technologies Inc. All rights reserved. -// See LICENSE for details. -// - -import Foundation -import WebKit - -class Client { - let passportHeader: String = "X-Optable-Visitor" - var storage: LocalStorage - var ua: String? - - init(_ config: OptableConfig) { - self.storage = LocalStorage(config) - if (config.customUserAgent == nil) { - self.userAgent { (realUserAgent) in - self.ua = realUserAgent - } - } else { - self.ua = config.customUserAgent - } - } - - func dispatchRequest(_ req: URLRequest, _ completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { - return URLSession.shared.dataTask(with: req) { (data, response, error) in - guard let res = response as? HTTPURLResponse, error == nil else { - completionHandler(data, response, error) - return - } - guard 200 ..< 300 ~= res.statusCode else { - completionHandler(data, response, error) - return - } - if #available(iOS 13.0, *) { - if let passport = res.value(forHTTPHeaderField: self.passportHeader) { - self.storage.setPassport(passport) - } - } else { - // In older versions of iOS, we have to resort searching through headers via res.allHeaderFields - // Unlike res.value(forHTTPHeaderField:...) which was introduced in iOS 13.0, allHeaderFields is - // case-sensitive, so we need to take special care to perform a case-INsensitive search: - for (key, value) in res.allHeaderFields { - if let header = key as? String { - let result: ComparisonResult = header.compare(self.passportHeader, options: NSString.CompareOptions.caseInsensitive) - if result == .orderedSame { - if let pp = value as? String { - self.storage.setPassport(pp) - break - } - } - } - } - } - completionHandler(data, response, error) - } - } - - func postRequest(url: URL, data: Any) throws -> URLRequest { - var req = URLRequest(url: url) - req.httpMethod = "POST" - - let reqBodyJSON = try JSONSerialization.data(withJSONObject: data, options: []) - req.httpBody = reqBodyJSON - - req.addValue("application/json", forHTTPHeaderField: "Content-Type") - req.addValue("application/json", forHTTPHeaderField: "Accept") - - if let passport: String = self.storage.getPassport() { - req.addValue(passport, forHTTPHeaderField: self.passportHeader) - } - - if let ua = self.ua { - req.addValue(ua, forHTTPHeaderField: "User-Agent") - } - - return req - } - - func getRequest(url: URL) throws -> URLRequest { - var req = URLRequest(url: url) - req.httpMethod = "GET" - - req.addValue("application/json", forHTTPHeaderField: "Content-Type") - req.addValue("application/json", forHTTPHeaderField: "Accept") - - if let passport: String = self.storage.getPassport() { - req.addValue(passport, forHTTPHeaderField: self.passportHeader) - } - - if let ua = self.ua { - req.addValue(ua, forHTTPHeaderField: "User-Agent") - } - - return req - } - - func userAgent(callback: @escaping(_ useragent: String) -> Void) { - var wkUserAgent: String = "" - let myGroup = DispatchGroup() - let window = UIApplication.shared.keyWindow - let webView = WKWebView(frame: UIScreen.main.bounds) - - webView.isHidden = true - window?.addSubview(webView) - myGroup.enter() - - webView.loadHTMLString("", baseURL: nil) - webView.evaluateJavaScript("navigator.userAgent", completionHandler: { (userAgent: Any?, error: Error?) in - if let userAgent = userAgent as? String { - wkUserAgent = userAgent - } - webView.stopLoading() - webView.removeFromSuperview() - myGroup.leave() - }) - myGroup.notify(queue: .main) { - callback(wkUserAgent) - } - } -} diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift new file mode 100644 index 0000000..c3fdd3b --- /dev/null +++ b/Source/Core/EdgeAPI.swift @@ -0,0 +1,150 @@ +// +// EdgeAPI.swift +// OptableSDK +// +// Created by user on 15.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +import Foundation +import WebKit + +final class EdgeAPI { + private let kPassportHeader: String = "X-Optable-Visitor" + + var storage: LocalStorage + var config: OptableConfig + + var userAgent: String? + + init(_ config: OptableConfig) { + self.config = config + self.storage = LocalStorage(config) + if config.customUserAgent == nil { + self.resolveUserAgent { realUserAgent in + self.userAgent = realUserAgent + } + } else { + self.userAgent = config.customUserAgent + } + } + + func profile(traits: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + guard let url = config.edgeURL("profile") else { return nil } + let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: ["traits": traits]) + return dispatchRequest(req, completionHandler) + } + + func targeting(completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + guard let url = config.edgeURL("targeting") else { return nil } + let req = try buildRequest(.GET, url: url, headers: resolveHeaders()) + return dispatchRequest(req, completionHandler) + } + + func identify(ids: [String], completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + guard let url = config.edgeURL("identify") else { return nil } + let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: ids) + return dispatchRequest(req, completionHandler) + } + + func witness(event: String, properties: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + guard let url = config.edgeURL("witness") else { return nil } + let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: ["event": event, "properties": properties]) + return dispatchRequest(req, completionHandler) + } + + // MARK: Private + private func dispatchRequest(_ req: URLRequest, _ completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { + return URLSession.shared.dataTask(with: req) { data, response, error in + guard let res = response as? HTTPURLResponse, error == nil else { + completionHandler(data, response, error) + return + } + guard 200 ..< 300 ~= res.statusCode else { + completionHandler(data, response, error) + return + } + if #available(iOS 13.0, *) { + if let passport = res.value(forHTTPHeaderField: self.kPassportHeader) { + self.storage.setPassport(passport) + } + } else { + // In older versions of iOS, we have to resort searching through headers via res.allHeaderFields + // Unlike res.value(forHTTPHeaderField:...) which was introduced in iOS 13.0, allHeaderFields is + // case-sensitive, so we need to take special care to perform a case-INsensitive search: + for (key, value) in res.allHeaderFields { + if let header = key as? String { + let result: ComparisonResult = header.compare(self.kPassportHeader, options: NSString.CompareOptions.caseInsensitive) + if result == .orderedSame { + if let pp = value as? String { + self.storage.setPassport(pp) + break + } + } + } + } + } + completionHandler(data, response, error) + } + } + + private func resolveUserAgent(callback: @escaping (_ useragent: String) -> Void) { + var wkUserAgent = "" + let myGroup = DispatchGroup() + let window = UIApplication.shared.keyWindow + let webView = WKWebView(frame: UIScreen.main.bounds) + + webView.isHidden = true + window?.addSubview(webView) + myGroup.enter() + + webView.loadHTMLString("", baseURL: nil) + webView.evaluateJavaScript("navigator.userAgent", completionHandler: { (userAgent: Any?, error: Error?) in + if let userAgent = userAgent as? String { + wkUserAgent = userAgent + } + webView.stopLoading() + webView.removeFromSuperview() + myGroup.leave() + }) + myGroup.notify(queue: .main) { + callback(wkUserAgent) + } + } + + private func resolveHeaders() -> HTTPHeaders { + var headers = HTTPHeaders() + headers[.accept] = "application/json" + headers[.contentType] = "application/json" + + if let ua = self.userAgent { + headers[.userAgent] = ua + } + + if let apiKey = config.apiKey { + headers[.authorization] = "Bearer \(apiKey)" + } + + if let passport: String = self.storage.getPassport() { + headers[kPassportHeader] = passport + } + + return headers + } + + private func buildRequest(_ method: HTTPMethod, url: URL, headers: HTTPHeaders, data: Any? = nil) throws -> URLRequest { + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + + if let data = data { + let reqBodyJSON = try JSONSerialization.data(withJSONObject: data, options: []) + request.httpBody = reqBodyJSON + } + + for (key, value) in headers.asDict { + request.addValue(value, forHTTPHeaderField: key) + } + + return request + } +} diff --git a/Source/Core/LocalStorage.swift b/Source/Core/LocalStorage.swift index 012c8bb..272da98 100644 --- a/Source/Core/LocalStorage.swift +++ b/Source/Core/LocalStorage.swift @@ -11,7 +11,7 @@ import Foundation -class LocalStorage: NSObject { +final class LocalStorage: NSObject { let keyPfx: String = "OPTABLE" var passportKey: String var targetingKey: String diff --git a/Source/Core/Networking.swift b/Source/Core/Networking.swift new file mode 100644 index 0000000..d66e5f1 --- /dev/null +++ b/Source/Core/Networking.swift @@ -0,0 +1,223 @@ +// +// Networking.swift +// OptableSDK +// +// Created by user on 15.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +import Foundation + +// MARK: - HTTPMethod +enum HTTPMethod: String { + case GET + case HEAD + case POST + case PUT + case DELETE + case CONNECT + case OPTIONS + case TRACE + case PATCH +} + +// MARK: - HTTPHeader +enum HTTPHeader: String { + // Content negotiation + case accept = "Accept" + case contentType = "Content-Type" + case contentLength = "Content-Length" + case contentEncoding = "Content-Encoding" + + // Authorization / security + case authorization = "Authorization" + case wwwAuthenticate = "WWW-Authenticate" + case proxyAuthorization = "Proxy-Authorization" + + // Caching + case cacheControl = "Cache-Control" + case pragma = "Pragma" + case expires = "Expires" + case etag = "ETag" + case ifNoneMatch = "If-None-Match" + case ifModifiedSince = "If-Modified-Since" + + // Connection + case connection = "Connection" + case keepAlive = "Keep-Alive" + case upgrade = "Upgrade" + + // User / client info + case userAgent = "User-Agent" + case referer = "Referer" + case origin = "Origin" + case host = "Host" + + // Cookies + case cookie = "Cookie" + case setCookie = "Set-Cookie" + + // Range / transfer + case range = "Range" + case acceptRanges = "Accept-Ranges" + case transferEncoding = "Transfer-Encoding" + + // CORS + case accessControlAllowOrigin = "Access-Control-Allow-Origin" + case accessControlAllowMethods = "Access-Control-Allow-Methods" + case accessControlAllowHeaders = "Access-Control-Allow-Headers" + case accessControlExposeHeaders = "Access-Control-Expose-Headers" + case accessControlAllowCredentials = "Access-Control-Allow-Credentials" + case accessControlMaxAge = "Access-Control-Max-Age" + + // Compression + case acceptEncoding = "Accept-Encoding" + case acceptLanguage = "Accept-Language" +} + +// MARK: - HTTPHeaders +struct HTTPHeaders { + private var dict: [String: String] = [:] + + var asDict: [String: String] { dict } + + init() {} + + init(_ dict: [String: String]) { + self.dict = dict + } + + subscript(_ key: HTTPHeader) -> String? { + get { dict[key.rawValue] } + set { dict[key.rawValue] = newValue } + } + + subscript(_ key: String) -> String? { + get { dict[key] } + set { dict[key] = newValue } + } +} + +// MARK: - HTTPQuery +enum HTTPQuery { + case jsonObject(Encodable) + case dict([String: String?]) +} + +// MARK: - HTTPBody +enum HTTPBody { + case jsonObject(Encodable) + case jsonArray([Any]) + case jsonDict([String: Any]) +} + +// MARK: - HTTPStatusCode +enum HTTPStatusCode { + // 1xx Informational + case `continue` // 100 + case switchingProtocols // 101 + case processing // 102 + + // 2xx Success + case ok // 200 + case created // 201 + case accepted // 202 + case nonAuthoritative // 203 + case noContent // 204 + case resetContent // 205 + case partialContent // 206 + + // 3xx Redirection + case multipleChoices // 300 + case movedPermanently // 301 + case found // 302 + case seeOther // 303 + case notModified // 304 + case temporaryRedirect // 307 + case permanentRedirect // 308 + + // 4xx Client Error + case badRequest // 400 + case unauthorized // 401 + case paymentRequired // 402 + case forbidden // 403 + case notFound // 404 + case methodNotAllowed // 405 + case notAcceptable // 406 + case conflict // 409 + case gone // 410 + case unsupportedMediaType // 415 + case tooManyRequests // 429 + + // 5xx Server Error + case internalServerError // 500 + case notImplemented // 501 + case badGateway // 502 + case serviceUnavailable // 503 + case gatewayTimeout // 504 + + // Categories + case informational // 100 ..< 200 + case successful // 200 ..< 300 + case redirect // 300 ..< 400 + case clientError // 400 ..< 500 + case serverError // 500 ..< 600 + + // swiftlint:disable:next cyclomatic_complexity + init(statusCode: Int) { + self = switch statusCode { + // 1xx + case 100: .continue + case 101: .switchingProtocols + case 102: .processing + // 2xx + case 200: .ok + case 201: .created + case 202: .accepted + case 203: .nonAuthoritative + case 204: .noContent + case 205: .resetContent + case 206: .partialContent + // 3xx + case 300: .multipleChoices + case 301: .movedPermanently + case 302: .found + case 303: .seeOther + case 304: .notModified + case 307: .temporaryRedirect + case 308: .permanentRedirect + // 4xx + case 400: .badRequest + case 401: .unauthorized + case 402: .paymentRequired + case 403: .forbidden + case 404: .notFound + case 405: .methodNotAllowed + case 406: .notAcceptable + case 409: .conflict + case 410: .gone + case 415: .unsupportedMediaType + case 429: .tooManyRequests + // 5xx + case 500: .internalServerError + case 501: .notImplemented + case 502: .badGateway + case 503: .serviceUnavailable + case 504: .gatewayTimeout + // Ranges + case 100 ..< 200: .informational + case 200 ..< 300: .successful + case 300 ..< 400: .redirect + case 400 ..< 500: .clientError + case 500 ..< 600: .serverError + default: + .serverError + } + } + + var isSuccess: Bool { + if case .successful = self { return true } + if case .ok = self { return true } + return false + } +} diff --git a/Source/Edge/Edge.swift b/Source/Edge/Edge.swift deleted file mode 100644 index e14e29c..0000000 --- a/Source/Edge/Edge.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Edge.swift -// OptableSDK -// -// Created by user on 15.12.2025. -// Copyright © 2025 Optable Technologies, Inc. All rights reserved. -// - -import Foundation - -enum Edge { - static func profile(config: OptableConfig, client: Client, traits: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.edgeURL("profile") else { return nil } - let req = try client.postRequest(url: url, data: ["traits": traits]) - return client.dispatchRequest(req, completionHandler) - } - - static func targeting(config: OptableConfig, client: Client, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.edgeURL("targeting") else { return nil } - let req = try client.getRequest(url: url) - return client.dispatchRequest(req, completionHandler) - } - - static func identify(config: OptableConfig, client: Client, ids: [String], completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.edgeURL("identify") else { return nil } - let req = try client.postRequest(url: url, data: ids) - return client.dispatchRequest(req, completionHandler) - } - - static func witness(config: OptableConfig, client: Client, event: String, properties: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.edgeURL("witness") else { return nil } - let req = try client.postRequest(url: url, data: ["event": event, "properties": properties]) - return client.dispatchRequest(req, completionHandler) - } -} diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index 5a8d7e2..0f0d3f8 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -63,7 +63,7 @@ public class OptableSDK: NSObject { } var config: OptableConfig - var client: Client + var api: EdgeAPI /// /// `OptableSDK` returns an instance of the SDK configured to talk to the sandbox specified by `OptableConfig`: @@ -71,16 +71,35 @@ public class OptableSDK: NSObject { @objc public init(config: OptableConfig) { self.config = config - self.client = Client(config) + self.api = EdgeAPI(config) } + /// + /// OptableSDK.version returns the SDK version as a String. The version is based on the short + /// version string set in the SDK project CFBundleShortVersionString. When the SDK is included via + /// Cocoapods, it will be set automatically on `pod install` according to the podspec version. + /// + static var version: String { + let sdkBundle = Bundle(for: OptableSDK.self) + + guard + let marketingVersion = sdkBundle.infoDictionary?["CFBundleShortVersionString"] as? String, + let buildNumber = sdkBundle.infoDictionary?["CFBundleVersion"] as? String + else { return "ios-unknown" } + + return ["ios", marketingVersion, buildNumber].joined(separator: "-") + } +} + +// MARK: - Interface +public extension OptableSDK { /// /// identify(ids, completion) issues a call to the Optable Sandbox "identify" API, passing the specified /// list of type-prefixed IDs. It is asynchronous, and on completion it will call the specified completion handler, passing /// it either the HTTPURLResponse on success, or an OptableError on failure. /// - public func identify(ids: [String], _ completion: @escaping (Result) -> Void) throws { - try Edge.identify(config: self.config, client: self.client, ids: ids) { data, response, error in + func identify(ids: [String], _ completion: @escaping (Result) -> Void) throws { + try api.identify(ids: ids) { data, response, error in guard let response = response as? HTTPURLResponse, error == nil, data != nil else { if let err = error { completion(.failure(OptableError.identify("Session error: \(err)"))) @@ -102,24 +121,6 @@ public class OptableSDK: NSObject { }?.resume() } - /// - /// identify(ids) is the "delegate variant" of the identify(ids, completion) method. It wraps the latter with - /// a delegator callback. - /// - /// This is the Objective-C compatible version of the identify(ids, completion) API. - /// - @objc - public func identify(_ ids: [String]) throws { - try self.identify(ids: ids) { result in - switch result { - case let .success(response): - self.delegate?.identifyOk(response) - case let .failure(error as NSError): - self.delegate?.identifyErr(error) - } - } - } - /// /// identify(email, aaid, ppid, completion) issues a call to the Optable Sandbox "identify" API, passing it the SHA-256 /// of the caller-provided 'email' and, when specified via the 'aaid' Boolean, the Apple ID For Advertising (IDFA) @@ -128,7 +129,7 @@ public class OptableSDK: NSObject { /// The identify method is asynchronous, and on completion it will call the specified completion handler, passing /// it either the HTTPURLResponse on success, or an OptableError on failure. /// - public func identify(email: String, aaid: Bool = false, ppid: String = "", _ completion: @escaping (Result) -> Void) throws { + func identify(email: String, aaid: Bool = false, ppid: String = "", _ completion: @escaping (Result) -> Void) throws { var ids = [String]() if email != "" { @@ -156,24 +157,6 @@ public class OptableSDK: NSObject { try self.identify(ids: ids, completion) } - /// - /// identify(email, aaid, ppid) is the "delegate variant" of the identify(email, aaid, ppid, completion) method. - /// It wraps the latter with a delegator callback. - /// - /// This is the Objective-C compatible version of the identify(email, aaid, ppid, completion) API. - /// - @objc - public func identify(_ email: String, aaid: Bool = false, ppid: String = "") throws { - try self.identify(email: email, aaid: aaid, ppid: ppid) { result in - switch result { - case let .success(response): - self.delegate?.identifyOk(response) - case let .failure(error as NSError): - self.delegate?.identifyErr(error) - } - } - } - /// /// targeting(completion) calls the Optable Sandbox "targeting" API, which returns the key-value targeting /// data matching the user/device/app. @@ -184,8 +167,8 @@ public class OptableSDK: NSObject { /// On success, this method will also cache the resulting targeting data in client storage, which can /// be access using targetingFromCache(), and cleared using targetingClearCache(). /// - public func targeting(_ completion: @escaping (Result) -> Void) throws { - try Edge.targeting(config: self.config, client: self.client) { data, response, error in + func targeting(_ completion: @escaping (Result) -> Void) throws { + try api.targeting { data, response, error in guard let response = response as? HTTPURLResponse, error == nil, data != nil else { if let err = error { completion(.failure(OptableError.targeting("Session error: \(err)"))) @@ -209,7 +192,7 @@ public class OptableSDK: NSObject { let result = keyvalues as? NSDictionary ?? NSDictionary() /// We cache the latest targeting result in client storage for targetingFromCache() users: - self.client.storage.setTargeting(keyvalues as? [String: Any] ?? [String: Any]()) + self.api.storage.setTargeting(keyvalues as? [String: Any] ?? [String: Any]()) completion(.success(result)) } catch { @@ -218,30 +201,12 @@ public class OptableSDK: NSObject { }?.resume() } - /// - /// targeting() is the "delegate variant" of the targeting(completion) method. It wraps the latter with - /// a delegator callback. - /// - /// This is the Objective-C compatible version of the targeting(completion) API. - /// - @objc - public func targeting() throws { - try self.targeting { result in - switch result { - case let .success(keyvalues): - self.delegate?.targetingOk(keyvalues) - case let .failure(error as NSError): - self.delegate?.targetingErr(error) - } - } - } - /// /// targetingFromCache() returns the previously cached targeting data, if any. /// @objc - public func targetingFromCache() -> NSDictionary? { - guard let keyvalues = self.client.storage.getTargeting() as NSDictionary? else { + func targetingFromCache() -> NSDictionary? { + guard let keyvalues = self.api.storage.getTargeting() as NSDictionary? else { return nil } return keyvalues @@ -251,8 +216,8 @@ public class OptableSDK: NSObject { /// targetingClearCache() clears any previously cached targeting data. /// @objc - public func targetingClearCache() { - self.client.storage.clearTargeting() + func targetingClearCache() { + self.api.storage.clearTargeting() } /// @@ -263,8 +228,8 @@ public class OptableSDK: NSObject { /// The witness method is asynchronous, and on completion it will call the specified completion handler, /// passing it either the HTTPURLResponse on success, or an OptableError on failure. /// - public func witness(event: String, properties: NSDictionary, _ completion: @escaping (Result) -> Void) throws { - try Edge.witness(config: self.config, client: self.client, event: event, properties: properties) { data, response, error in + func witness(event: String, properties: NSDictionary, _ completion: @escaping (Result) -> Void) throws { + try api.witness(event: event, properties: properties) { data, response, error in guard let response = response as? HTTPURLResponse, error == nil else { if let err = error { completion(.failure(OptableError.witness("Session error: \(err)"))) @@ -286,24 +251,6 @@ public class OptableSDK: NSObject { }?.resume() } - /// - /// witness(event, properties) is the "delegate variant" of the witness(event, properties, completion) method. - /// It wraps the latter with a delegator callback. - /// - /// This is the Objective-C compatible version of the witness(event, properties, completion) API. - /// - @objc - public func witness(_ event: String, properties: NSDictionary) throws { - try self.witness(event: event, properties: properties) { result in - switch result { - case let .success(response): - self.delegate?.witnessOk(response) - case let .failure(error as NSError): - self.delegate?.witnessErr(error) - } - } - } - /// /// profile(traits, completion) calls the Optable Sandbox "profile" API in order to associate /// specified 'traits' (i.e., key-value pairs) with the user's device. The specified @@ -312,8 +259,8 @@ public class OptableSDK: NSObject { /// The profile method is asynchronous, and on completion it will call the specified completion handler, /// passing it either the HTTPURLResponse on success, or an OptableError on failure. /// - public func profile(traits: NSDictionary, _ completion: @escaping (Result) -> Void) throws { - try Edge.profile(config: self.config, client: self.client, traits: traits) { data, response, error in + func profile(traits: NSDictionary, _ completion: @escaping (Result) -> Void) throws { + try api.profile(traits: traits) { data, response, error in guard let response = response as? HTTPURLResponse, error == nil else { if let err = error { completion(.failure(OptableError.profile("Session error: \(err)"))) @@ -335,29 +282,11 @@ public class OptableSDK: NSObject { }?.resume() } - /// - /// profile(traits) is the "delegate variant" of the profile(traits, completion) method. - /// It wraps the latter with a delegator callback. - /// - /// This is the Objective-C compatible version of the profile(traits, completion) API. - /// - @objc - public func profile(traits: NSDictionary) throws { - try self.profile(traits: traits) { result in - switch result { - case let .success(response): - self.delegate?.profileOk(response) - case let .failure(error as NSError): - self.delegate?.profileErr(error) - } - } - } - /// /// eid(email) is a helper that returns type-prefixed SHA256(downcase(email)) /// @objc - public func eid(_ email: String) -> String { + func eid(_ email: String) -> String { let pfx = "e:" let normEmail = Data(email.lowercased().trimmingCharacters(in: .whitespacesAndNewlines).utf8) @@ -389,7 +318,7 @@ public class OptableSDK: NSObject { /// aaid(idfa) is a helper that returns the type-prefixed Apple ID For Advertising /// @objc - public func aaid(_ idfa: String) -> String { + func aaid(_ idfa: String) -> String { return "a:" + idfa.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) } @@ -397,7 +326,7 @@ public class OptableSDK: NSObject { /// cid(ppid) is a helper that returns custom type-prefixed origin-provided PPID /// @objc - public func cid(_ ppid: String) -> String { + func cid(_ ppid: String) -> String { return "c:" + ppid.trimmingCharacters(in: .whitespacesAndNewlines) } @@ -412,7 +341,7 @@ public class OptableSDK: NSObject { /// hashed Email values can be used in calls to identify() /// @objc - public func eidFromURL(_ urlString: String) -> String { + func eidFromURL(_ urlString: String) -> String { guard let url = URL(string: urlString) else { return "" } guard let urlc = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return "" } guard let urlqis = urlc.queryItems else { return "" } @@ -449,27 +378,107 @@ public class OptableSDK: NSObject { /// links in newsletter Emails sent by the application developer. /// @objc - public func tryIdentifyFromURL(_ urlString: String) throws { + func tryIdentifyFromURL(_ urlString: String) throws { let oeid = self.eidFromURL(urlString) if !oeid.isEmpty { try self.identify(ids: [oeid]) { _ in /* no-op */ } } } +} +// MARK: - ObjectiveC support +public extension OptableSDK { /// - /// OptableSDK.version returns the SDK version as a String. The version is based on the short - /// version string set in the SDK project CFBundleShortVersionString. When the SDK is included via - /// Cocoapods, it will be set automatically on `pod install` according to the podspec version. + /// identify(ids) is the "delegate variant" of the identify(ids, completion) method. It wraps the latter with + /// a delegator callback. /// - public static var version: String { - let sdkBundle = Bundle(for: OptableSDK.self) + /// This is the Objective-C compatible version of the identify(ids, completion) API. + /// + @objc + func identify(_ ids: [String]) throws { + try self.identify(ids: ids) { result in + switch result { + case let .success(response): + self.delegate?.identifyOk(response) + case let .failure(error as NSError): + self.delegate?.identifyErr(error) + } + } + } - guard - let marketingVersion = sdkBundle.infoDictionary?["CFBundleShortVersionString"] as? String, - let buildNumber = sdkBundle.infoDictionary?["CFBundleVersion"] as? String - else { return "ios-unknown" } + /// + /// identify(email, aaid, ppid) is the "delegate variant" of the identify(email, aaid, ppid, completion) method. + /// It wraps the latter with a delegator callback. + /// + /// This is the Objective-C compatible version of the identify(email, aaid, ppid, completion) API. + /// + @objc + func identify(_ email: String, aaid: Bool = false, ppid: String = "") throws { + try self.identify(email: email, aaid: aaid, ppid: ppid) { result in + switch result { + case let .success(response): + self.delegate?.identifyOk(response) + case let .failure(error as NSError): + self.delegate?.identifyErr(error) + } + } + } - return ["ios", marketingVersion, buildNumber].joined(separator: "-") + /// + /// targeting() is the "delegate variant" of the targeting(completion) method. It wraps the latter with + /// a delegator callback. + /// + /// This is the Objective-C compatible version of the targeting(completion) API. + /// + @objc + func targeting() throws { + try self.targeting { result in + switch result { + case let .success(keyvalues): + self.delegate?.targetingOk(keyvalues) + case let .failure(error as NSError): + self.delegate?.targetingErr(error) + } + } + } + + /// + /// witness(event, properties) is the "delegate variant" of the witness(event, properties, completion) method. + /// It wraps the latter with a delegator callback. + /// + /// This is the Objective-C compatible version of the witness(event, properties, completion) API. + /// + @objc + func witness(_ event: String, properties: NSDictionary) throws { + try self.witness(event: event, properties: properties) { result in + switch result { + case let .success(response): + self.delegate?.witnessOk(response) + case let .failure(error as NSError): + self.delegate?.witnessErr(error) + } + } + } + + /// + /// profile(traits) is the "delegate variant" of the profile(traits, completion) method. + /// It wraps the latter with a delegator callback. + /// + /// This is the Objective-C compatible version of the profile(traits, completion) API. + /// + @objc + func profile(traits: NSDictionary) throws { + try self.profile(traits: traits) { result in + switch result { + case let .success(response): + self.delegate?.profileOk(response) + case let .failure(error as NSError): + self.delegate?.profileErr(error) + } + } } } + +// MARK: - Swift Concurrency support +public extension OptableSDK { /* TODO: */ } From a9e3e1df88cb1c2927b996089cb36df039bf9c49 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 15 Dec 2025 19:14:08 +0100 Subject: [PATCH 05/42] - Fixed naming --- Source/Core/EdgeAPI.swift | 7 ++++--- Source/OptableConfig.swift | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index c3fdd3b..5276f01 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -29,6 +29,7 @@ final class EdgeAPI { } } + // MARK: Endpoints func profile(traits: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { guard let url = config.edgeURL("profile") else { return nil } let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: ["traits": traits]) @@ -117,15 +118,15 @@ final class EdgeAPI { headers[.accept] = "application/json" headers[.contentType] = "application/json" - if let ua = self.userAgent { - headers[.userAgent] = ua + if let userAgent { + headers[.userAgent] = userAgent } if let apiKey = config.apiKey { headers[.authorization] = "Bearer \(apiKey)" } - if let passport: String = self.storage.getPassport() { + if let passport: String = storage.getPassport() { headers[kPassportHeader] = passport } diff --git a/Source/OptableConfig.swift b/Source/OptableConfig.swift index f130fa1..7ee22db 100644 --- a/Source/OptableConfig.swift +++ b/Source/OptableConfig.swift @@ -32,7 +32,7 @@ public class OptableConfig: NSObject { public var customUserAgent: String? /// Boolean flag to skip the detection of advertising IDs. Default is false. - public var skipAdvertisingIdDetection: Bool // FIXME: is really used? + public var skipAdvertisingIdDetection: Bool public init( tenant: String, From dc41453f3e0844d2e33d3232ce1688e4a3fa7631 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 15 Dec 2025 19:14:20 +0100 Subject: [PATCH 06/42] - Added AppTrackingTransparency --- Source/Core/AppTrackingTransparency.swift | 88 +++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 Source/Core/AppTrackingTransparency.swift diff --git a/Source/Core/AppTrackingTransparency.swift b/Source/Core/AppTrackingTransparency.swift new file mode 100644 index 0000000..a536db4 --- /dev/null +++ b/Source/Core/AppTrackingTransparency.swift @@ -0,0 +1,88 @@ +// +// AppTrackingTransparency.swift +// OptableSDK +// +// Created by user on 15.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +#if canImport(AdSupport) + + import AdSupport + + #if canImport(AppTrackingTransparency) + import AppTrackingTransparency + #endif + + import Foundation + + enum ATT { + static var advertisingIdentifier: UUID { + ASIdentifierManager.shared().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 + } + + static var adfaAvailable: Bool { + #if canImport(AppTrackingTransparency) + if #available(iOS 14, *) { + return trackingStatus == .authorized + } else { + return isAdvertisingTrackingEnabled + } + #else + return isAdvertisingTrackingEnabled + #endif + } + + static var attAvailable: Bool { + if #available(iOS 14, *) { + return true + } else { + return false + } + } + + #if canImport(AppTrackingTransparency) + + static var canAuthorize: Bool { + if #available(iOS 14, *) { + return ATTrackingManager.trackingAuthorizationStatus == .notDetermined + } else { + return false + } + } + + @available(iOS 14, *) + static var trackingStatus: ATTrackingManager.AuthorizationStatus { + ATTrackingManager.trackingAuthorizationStatus + } + + @available(iOS 14, *) + static func requestATTAuthorization(completion: ((Bool) -> Void)? = nil) { + ATTrackingManager.requestTrackingAuthorization { status in + switch status { + case .authorized: completion?(true) + case .denied, .notDetermined, .restricted: completion?(false) + @unknown default: completion?(true) + } + } + } + + @available(iOS 14, *) + @discardableResult + static func requestATTAuthorization() async -> Bool { + await withCheckedContinuation({ continuation in + requestATTAuthorization(completion: { isAuthorized in + continuation.resume(returning: isAuthorized) + }) + }) + } + + #endif + } + +#endif From 20484caf966f4de0e9de09f6f8b6cb3b4fce58ed Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 15 Dec 2025 19:15:04 +0100 Subject: [PATCH 07/42] - Added OptableIdentifiers --- Source/Core/OptableIdentifiers.swift | 109 +++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 Source/Core/OptableIdentifiers.swift diff --git a/Source/Core/OptableIdentifiers.swift b/Source/Core/OptableIdentifiers.swift new file mode 100644 index 0000000..a16ecea --- /dev/null +++ b/Source/Core/OptableIdentifiers.swift @@ -0,0 +1,109 @@ +// +// OptableIdentifiers.swift +// OptableSDK +// +// Created by user on 15.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +import Foundation + +// MARK: - OptableIdentifier +enum OptableIdentifier: RawRepresentable, Hashable { + // Personal identifiers + case emailAddress // e + case phoneNumber // p + case postalCode // z + + // IP addresses + case ipv4Address // i4 + case ipv6Address // i6 + + // Device IDs + case appleIDFA // a + case googleGAID // g + case rokuRIDA // r + case samsungTIFA // s + case amazonFireAFAI // f + + // Universal / identity frameworks + case netID // n + case id5 // id5 + case utiq // utiq + + // Custom IDs (c, c1...cN) + case custom(Int?) // nil = "c", 1..N = "c1"..."cN" + + // Optable VID + case optableVID // v + + // MARK: RawRepresentable + init?(rawValue: String) { + switch rawValue { + case "e": self = .emailAddress + case "p": self = .phoneNumber + case "z": self = .postalCode + case "i4": self = .ipv4Address + case "i6": self = .ipv6Address + case "a": self = .appleIDFA + case "g": self = .googleGAID + case "r": self = .rokuRIDA + case "s": self = .samsungTIFA + case "f": self = .amazonFireAFAI + case "n": self = .netID + case "id5": self = .id5 + case "utiq": self = .utiq + case "c": self = .custom(nil) + case "v": self = .optableVID + default: + if rawValue.starts(with: "c"), + let number = Int(rawValue.dropFirst()) { + self = .custom(number) + } else { + return nil + } + } + } + + var rawValue: String { + switch self { + case .emailAddress: return "e" + case .phoneNumber: return "p" + case .postalCode: return "z" + case .ipv4Address: return "i4" + case .ipv6Address: return "i6" + case .appleIDFA: return "a" + case .googleGAID: return "g" + case .rokuRIDA: return "r" + case .samsungTIFA: return "s" + case .amazonFireAFAI: return "f" + case .netID: return "n" + case .id5: return "id5" + case .utiq: return "utiq" + case .custom(nil): return "c" + case let .custom(n?): return "c\(n)" + case .optableVID: return "v" + } + } +} + +// MARK: - OptableIdentifiers +struct OptableIdentifiers { + var dict: [String: String] = [:] + + init() {} + + init(_ dict: [String: String]) { + self.dict = dict + } + + subscript(_ key: OptableIdentifier) -> String? { + get { dict[key.rawValue] } + set { dict[key.rawValue] = newValue } + } + + subscript(_ key: String) -> String? { + get { dict[key] } + set { dict[key] = newValue } + } +} From a3bb5df7f9286144997c5e2ff962f1cf86d4a721 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Tue, 16 Dec 2025 15:49:24 +0100 Subject: [PATCH 08/42] - Updated bundle --- Gemfile | 2 +- Gemfile.lock | 289 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 169 insertions(+), 122 deletions(-) diff --git a/Gemfile b/Gemfile index a67f32a..c5d4f97 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" gem 'fastlane' -gem "cocoapods", "= 1.12.0" +gem "cocoapods" diff --git a/Gemfile.lock b/Gemfile.lock index 967e6f3..44c02b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,45 +1,57 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.5) - rexml - activesupport (6.1.7.9) - concurrent-ruby (~> 1.0, >= 1.0.2) + CFPropertyList (3.0.8) + abbrev (0.1.2) + activesupport (7.2.3) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.8) + public_suffix (>= 2.0.2, < 8.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.544.0) - aws-sdk-core (3.125.0) - aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.525.0) - aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.53.0) - aws-sdk-core (~> 3, >= 3.125.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.110.0) - aws-sdk-core (~> 3, >= 3.125.0) + aws-eventstream (1.4.0) + aws-partitions (1.1194.0) + aws-sdk-core (3.239.2) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.207.0) + aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sigv4 (1.4.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - claide (1.0.3) - cocoapods (1.12.0) + base64 (0.2.0) + benchmark (0.5.0) + bigdecimal (3.3.1) + claide (1.1.0) + cocoapods (1.16.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.12.0) + cocoapods-core (= 1.16.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.6.0, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-trunk (>= 1.6.0, < 2.0) @@ -51,8 +63,8 @@ GEM molinillo (~> 0.8.0) nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.12.0) + xcodeproj (>= 1.27.0, < 2.0) + cocoapods-core (1.16.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -63,7 +75,7 @@ GEM public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) + cocoapods-downloader (2.1) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -75,52 +87,61 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.3.4) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + csv (3.3.5) declarative (0.0.20) - digest-crc (0.6.4) + digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.6) + domain_name (0.6.20240107) + dotenv (2.8.1) + drb (2.2.3) emoji_regex (3.2.3) escape (0.0.4) - ethon (0.16.0) + ethon (0.15.0) ffi (>= 1.15.0) - excon (0.89.0) - faraday (1.8.0) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.1) + faraday-net_http_persistent (~> 1.0) faraday-patron (~> 1.0) faraday-rack (~> 1.0) - multipart-post (>= 1.2, < 3) + faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) + faraday-cookie_jar (0.0.8) faraday (>= 0.8.0) - http-cookie (~> 1.0.0) + http-cookie (>= 1.0.0) faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) + faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-net_http (1.0.1) + faraday-multipart (1.1.1) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday_middleware (1.2.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.2.6) - fastlane (2.199.0) + fastimage (2.4.0) + fastlane (2.229.1) CFPropertyList (>= 2.3, < 4.0.0) + abbrev (~> 0.1.2) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) + csv (~> 3.3) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) excon (>= 0.71.0, < 1.0.0) @@ -128,36 +149,53 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) + http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (~> 2.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3.0) naturally (~> 2.2) - optparse (~> 0.1.1) + nkf (~> 0.2.0) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (>= 1.4.5, < 2.0.0) + terminal-table (~> 3) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) - ffi (1.17.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + ffi (1.17.2) + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-aarch64-linux-musl) + ffi (1.17.2-arm-linux-gnu) + ffi (1.17.2-arm-linux-musl) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86-linux-gnu) + ffi (1.17.2-x86-linux-musl) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.2-x86_64-linux-musl) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.14.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-core (0.4.1) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -165,116 +203,125 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick - google-apis-iamcredentials_v1 (0.9.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.6.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.10.0) - google-apis-core (>= 0.4, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.8.0) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (1.5.0) - faraday (>= 0.17.3, < 2.0) - google-cloud-errors (1.2.0) - google-cloud-storage (1.35.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.5.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.1.0) - faraday (>= 0.17.3, < 2.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.4) + http-cookie (1.0.8) domain_name (~> 0.5) - httpclient (2.8.3) - i18n (1.14.6) + httpclient (2.9.0) + mutex_m + i18n (1.14.7) concurrent-ruby (~> 1.0) - jmespath (1.4.0) - json (2.6.1) - jwt (2.3.0) - memoist (0.16.2) - mini_magick (4.11.0) - mini_mime (1.1.2) - minitest (5.25.1) + jmespath (1.6.2) + json (2.18.0) + jwt (2.10.2) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + minitest (5.27.0) molinillo (0.8.0) - multi_json (1.15.0) - multipart-post (2.0.0) - nanaimo (0.3.0) + multi_json (1.18.0) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) nap (1.1.0) - naturally (2.2.1) + naturally (2.3.0) netrc (0.11.0) - optparse (0.1.1) + nkf (0.2.0) + optparse (0.8.1) os (1.1.4) - plist (3.6.0) - public_suffix (4.0.6) - rake (13.0.6) - representable (3.1.1) + plist (3.7.2) + public_suffix (4.0.7) + rake (13.3.1) + representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.5) - rouge (2.0.7) + rexml (3.4.4) + rouge (3.28.0) ruby-macho (2.5.1) ruby2_keywords (0.0.5) - rubyzip (2.3.2) - security (0.1.3) - signet (0.16.0) + rubyzip (2.4.1) + securerandom (0.4.1) + security (0.1.5) + signet (0.21.0) addressable (~> 2.8) - faraday (>= 0.17.3, < 2.0) - jwt (>= 1.5, < 3.0) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) multi_json (~> 1.10) - simctl (1.6.8) + simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) - typhoeus (1.4.1) - ethon (>= 0.9.0) + typhoeus (1.5.0) + ethon (>= 0.9.0, < 0.16.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8) - unicode-display_width (1.8.0) - webrick (1.7.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.21.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) - xcpretty (0.3.0) - rouge (~> 2.0.7) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) - zeitwerk (2.6.18) PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin ruby + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES - cocoapods (= 1.12.0) + cocoapods fastlane BUNDLED WITH - 2.3.3 + 2.7.2 From a7addf801346425d454a6c8856466ef318195000 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Tue, 16 Dec 2025 18:18:53 +0100 Subject: [PATCH 09/42] - Introduced OptableIdentifierEncoder, OptableIdentifier; Updated OptableSDK interface with async/await support --- Source/Core/EdgeAPI.swift | 46 +- Source/Core/LocalStorage.swift | 6 +- Source/Core/OptableError.swift | 27 + Source/Core/OptableIdentifierEncoder.swift | 122 ++++ Source/Core/OptableIdentifiers.swift | 96 +--- Source/OptableConfig.swift | 41 +- Source/OptableIdentifier.swift | 86 +++ Source/OptableSDK.swift | 636 ++++++++++----------- 8 files changed, 618 insertions(+), 442 deletions(-) create mode 100644 Source/Core/OptableError.swift create mode 100644 Source/Core/OptableIdentifierEncoder.swift create mode 100644 Source/OptableIdentifier.swift diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index 5276f01..ae60765 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -17,6 +17,8 @@ final class EdgeAPI { var userAgent: String? + private lazy var jsonEncoder = JSONEncoder() + init(_ config: OptableConfig) { self.config = config self.storage = LocalStorage(config) @@ -30,27 +32,28 @@ final class EdgeAPI { } // MARK: Endpoints - func profile(traits: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.edgeURL("profile") else { return nil } - let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: ["traits": traits]) + func identify(ids: OptableIdentifiers, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + guard let url = config.buildEdgeURL("identify") else { return nil } + let jsonData = try jsonEncoder.encode(ids) + let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: jsonData) return dispatchRequest(req, completionHandler) } - func targeting(completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.edgeURL("targeting") else { return nil } - let req = try buildRequest(.GET, url: url, headers: resolveHeaders()) + func profile(traits: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + guard let url = config.buildEdgeURL("profile") else { return nil } + let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), obj: ["traits": traits]) return dispatchRequest(req, completionHandler) } - func identify(ids: [String], completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.edgeURL("identify") else { return nil } - let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: ids) + func targeting(completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + guard let url = config.buildEdgeURL("targeting") else { return nil } + let req = try buildRequest(.GET, url: url, headers: resolveHeaders()) return dispatchRequest(req, completionHandler) } func witness(event: String, properties: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.edgeURL("witness") else { return nil } - let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: ["event": event, "properties": properties]) + guard let url = config.buildEdgeURL("witness") else { return nil } + let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), obj: ["event": event, "properties": properties]) return dispatchRequest(req, completionHandler) } @@ -133,12 +136,12 @@ final class EdgeAPI { return headers } - private func buildRequest(_ method: HTTPMethod, url: URL, headers: HTTPHeaders, data: Any? = nil) throws -> URLRequest { + private func buildRequest(_ method: HTTPMethod, url: URL, headers: HTTPHeaders, obj: Any? = nil) throws -> URLRequest { var request = URLRequest(url: url) request.httpMethod = method.rawValue - if let data = data { - let reqBodyJSON = try JSONSerialization.data(withJSONObject: data, options: []) + if let obj = obj { + let reqBodyJSON = try JSONSerialization.data(withJSONObject: obj, options: []) request.httpBody = reqBodyJSON } @@ -148,4 +151,19 @@ final class EdgeAPI { return request } + + private func buildRequest(_ method: HTTPMethod, url: URL, headers: HTTPHeaders, data: Data? = nil) throws -> URLRequest { + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + + if let data { + request.httpBody = data + } + + for (key, value) in headers.asDict { + request.addValue(value, forHTTPHeaderField: key) + } + + return request + } } diff --git a/Source/Core/LocalStorage.swift b/Source/Core/LocalStorage.swift index 272da98..8c1ee0f 100644 --- a/Source/Core/LocalStorage.swift +++ b/Source/Core/LocalStorage.swift @@ -2,15 +2,15 @@ // LocalStorage.swift // OptableSDK // -// The OptableSDK keeps some state in UserDefaults (https://developer.apple.com/documentation/foundation/userdefaults), a key/value store persisted -// across launches of the app. The state is therefore unique to the app+device, and not globally unique to the app across devices. -// // Copyright © 2020 Optable Technologies Inc. All rights reserved. // See LICENSE for details. // import Foundation +/** + The OptableSDK keeps some state in UserDefaults (https://developer.apple.com/documentation/foundation/userdefaults), a key/value store persisted across launches of the app. The state is therefore unique to the app+device, and not globally unique to the app across devices. + */ final class LocalStorage: NSObject { let keyPfx: String = "OPTABLE" var passportKey: String diff --git a/Source/Core/OptableError.swift b/Source/Core/OptableError.swift new file mode 100644 index 0000000..ac6b666 --- /dev/null +++ b/Source/Core/OptableError.swift @@ -0,0 +1,27 @@ +// +// OptableError.swift +// OptableSDK +// +// Created by user on 16.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +import Foundation + +enum OptableError { + static func identify(_ message: String, code: Int = -1) -> NSError { + NSError(domain: "OptableSDK.identify", code: code, userInfo: [NSLocalizedDescriptionKey: message]) + } + + static func profile(_ message: String, code: Int = -1) -> NSError { + NSError(domain: "OptableSDK.profile", code: code, userInfo: [NSLocalizedDescriptionKey: message]) + } + + static func targeting(_ message: String, code: Int = -1) -> NSError { + NSError(domain: "OptableSDK.targeting", code: code, userInfo: [NSLocalizedDescriptionKey: message]) + } + + static func witness(_ message: String, code: Int = -1) -> NSError { + NSError(domain: "OptableSDK.witness", code: code, userInfo: [NSLocalizedDescriptionKey: message]) + } +} diff --git a/Source/Core/OptableIdentifierEncoder.swift b/Source/Core/OptableIdentifierEncoder.swift new file mode 100644 index 0000000..9542610 --- /dev/null +++ b/Source/Core/OptableIdentifierEncoder.swift @@ -0,0 +1,122 @@ +// +// OptableIdentifierEncoder.swift +// OptableSDK +// +// Created by user on 16.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +import CommonCrypto +import Foundation +#if canImport(CryptoKit) + import CryptoKit +#endif + +// MARK: - OptableIdentifierEncoder +@objc +final class OptableIdentifierEncoder: NSObject { + /// + /// aaid(idfa) is a helper that returns the type-prefixed Apple ID For Advertising + /// + @objc + func aaid(_ idfa: String) -> String { + return "a:" + idfa.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + } + + /// + /// cid(ppid) is a helper that returns custom type-prefixed origin-provided PPID + /// + @objc + func cid(_ ppid: String) -> String { + return "c:" + ppid.trimmingCharacters(in: .whitespacesAndNewlines) + } + + /// + /// eid(email) is a helper that returns type-prefixed SHA256(downcase(email)) + /// + @objc + func eid(_ email: String) -> String { + let pfx = "e:" + let normEmail = Data(email.lowercased().trimmingCharacters(in: .whitespacesAndNewlines).utf8) + + #if canImport(CryptoKit) + if #available(iOS 13.0, *) { + return pfx + SHA256.hash(data: normEmail).compactMap { + String(format: "%02x", $0) + }.joined() + } else { + return pfx + self.cchash(normEmail) + } + #else + return pfx + self.cchash(normEmail) + #endif + } + + @objc + func cchash(_ input: Data) -> String { + var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + input.withUnsafeBytes { bytes in + _ = CC_SHA256(bytes.baseAddress, CC_LONG(input.count), &digest) + } + return digest.makeIterator().compactMap { + String(format: "%02x", $0) + }.joined() + } + + /// + /// eidFromURL(urlString) is a helper that returns a type-prefixed ID based on + /// the query string oeid=sha256value parameter in the specified urlString, if + /// one is found. Otherwise, it returns an empty string. + /// + /// The use for this is when handling incoming universal links which might + /// contain an "oeid" value with the SHA256(downcase(email)) of a user, such as + /// encoded links in newsletter Emails sent by the application developer. Such + /// hashed Email values can be used in calls to identify() + /// + @objc + func eidFromURL(_ urlString: String) -> String { + guard let url = URL(string: urlString) else { return "" } + guard let urlc = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return "" } + guard let urlqis = urlc.queryItems else { return "" } + + /// Look for an oeid parameter in the urlString: + var oeid = "" + for qi: URLQueryItem in urlqis { + guard let val = qi.value else { + continue + } + if qi.name.lowercased() == "oeid" { + oeid = val + break + } + } + + /// Check that oeid looks like a valid SHA256: + let range = NSRange(location: 0, length: oeid.utf16.count) + guard let regex = try? NSRegularExpression(pattern: "[a-f0-9]{64}", options: .caseInsensitive) else { return "" } + if (oeid.count != 64) || (regex.firstMatch(in: oeid, options: [], range: range) == nil) { + return "" + } + + return "e:" + oeid.lowercased() + } + + /// + /// tryIdentifyFromURL(urlString) is a helper that attempts to find a valid-looking + /// "oeid" parameter in the specified urlString's query string parameters and, if found, + /// calls self.identify([oeid]). + /// + /// The use for this is when handling incoming universal links which might contain an + /// "oeid" value with the SHA256(downcase(email)) of an incoming user, such as encoded + /// links in newsletter Emails sent by the application developer. + /// + // TODO: to remove +// @objc +// func tryIdentifyFromURL(_ urlString: String) throws { +// let oeid = self.eidFromURL(urlString) +// +// if !oeid.isEmpty { +// try self.identify(ids: [oeid]) { _ in /* no-op */ } +// } +// } +} diff --git a/Source/Core/OptableIdentifiers.swift b/Source/Core/OptableIdentifiers.swift index a16ecea..7d8026a 100644 --- a/Source/Core/OptableIdentifiers.swift +++ b/Source/Core/OptableIdentifiers.swift @@ -8,85 +8,6 @@ import Foundation -// MARK: - OptableIdentifier -enum OptableIdentifier: RawRepresentable, Hashable { - // Personal identifiers - case emailAddress // e - case phoneNumber // p - case postalCode // z - - // IP addresses - case ipv4Address // i4 - case ipv6Address // i6 - - // Device IDs - case appleIDFA // a - case googleGAID // g - case rokuRIDA // r - case samsungTIFA // s - case amazonFireAFAI // f - - // Universal / identity frameworks - case netID // n - case id5 // id5 - case utiq // utiq - - // Custom IDs (c, c1...cN) - case custom(Int?) // nil = "c", 1..N = "c1"..."cN" - - // Optable VID - case optableVID // v - - // MARK: RawRepresentable - init?(rawValue: String) { - switch rawValue { - case "e": self = .emailAddress - case "p": self = .phoneNumber - case "z": self = .postalCode - case "i4": self = .ipv4Address - case "i6": self = .ipv6Address - case "a": self = .appleIDFA - case "g": self = .googleGAID - case "r": self = .rokuRIDA - case "s": self = .samsungTIFA - case "f": self = .amazonFireAFAI - case "n": self = .netID - case "id5": self = .id5 - case "utiq": self = .utiq - case "c": self = .custom(nil) - case "v": self = .optableVID - default: - if rawValue.starts(with: "c"), - let number = Int(rawValue.dropFirst()) { - self = .custom(number) - } else { - return nil - } - } - } - - var rawValue: String { - switch self { - case .emailAddress: return "e" - case .phoneNumber: return "p" - case .postalCode: return "z" - case .ipv4Address: return "i4" - case .ipv6Address: return "i6" - case .appleIDFA: return "a" - case .googleGAID: return "g" - case .rokuRIDA: return "r" - case .samsungTIFA: return "s" - case .amazonFireAFAI: return "f" - case .netID: return "n" - case .id5: return "id5" - case .utiq: return "utiq" - case .custom(nil): return "c" - case let .custom(n?): return "c\(n)" - case .optableVID: return "v" - } - } -} - // MARK: - OptableIdentifiers struct OptableIdentifiers { var dict: [String: String] = [:] @@ -97,6 +18,10 @@ struct OptableIdentifiers { self.dict = dict } + init(_ dict: [OptableIdentifier: String]) { + self.dict = Dictionary(uniqueKeysWithValues: dict.map({ ($0.key.rawValue, $0.value) })) + } + subscript(_ key: OptableIdentifier) -> String? { get { dict[key.rawValue] } set { dict[key.rawValue] = newValue } @@ -107,3 +32,16 @@ struct OptableIdentifiers { set { dict[key] = newValue } } } + +// MARK: - Encodable +extension OptableIdentifiers: Encodable { + func encode(to encoder: any Encoder) throws { + guard dict.isEmpty == false else { return } + + var container = encoder.unkeyedContainer() + for (key, value) in dict { + let enrichedIdentifier = "\(key):\(value)" + try container.encode(enrichedIdentifier) + } + } +} diff --git a/Source/OptableConfig.swift b/Source/OptableConfig.swift index 7ee22db..fa44be0 100644 --- a/Source/OptableConfig.swift +++ b/Source/OptableConfig.swift @@ -11,29 +11,60 @@ import Foundation @objc public class OptableConfig: NSObject { /// The tenant name associated with the configuration. E.g. `acmeco.optable.co` => `acmeco`. + @objc public var tenant: String /// The DCN's Source Slug. E.g. `acmeco-sdk`. + @objc public var originSlug: String /// The hostname of the Optable endpoint. Default value is "na.edge.optable.co". - public var host: String + @objc + public var host: String = "na.edge.optable.co" /// The API path to be appended to the host. Default value is "v2". - public var path: String + @objc + public var path: String = "v2" /// Boolean flag that determines if insecure HTTP should be used instead of HTTPS. Default is false. - public var insecure: Bool + @objc + public var insecure: Bool = false /// An optional API key for authentication. If the API Endpoint is enabled as private, a Service Account API key will be required. + @objc public var apiKey: String? /// An optional custom user agent string for network requests. + @objc public var customUserAgent: String? /// Boolean flag to skip the detection of advertising IDs. Default is false. - public var skipAdvertisingIdDetection: Bool + @objc + public var skipAdvertisingIdDetection: Bool = false + /** + - Parameters: + - tenant: The tenant name associated with the configuration. E.g. `acmeco.optable.co` => `acmeco`. + - originSlug: The DCN's Source Slug. E.g. `acmeco-sdk`. + */ + @objc + public init(tenant: String, originSlug: String) { + self.tenant = tenant + self.originSlug = originSlug + super.init() + } + + /** + - Parameters: + - tenant: The tenant name associated with the configuration. E.g. `acmeco.optable.co` => `acmeco`. + - originSlug: The DCN's Source Slug. E.g. `acmeco-sdk`. + - host: The hostname of the Optable endpoint. Default value is "na.edge.optable.co". + - path: The API path to be appended to the host. Default value is "v2". + - insecure: Boolean flag that determines if insecure HTTP should be used instead of HTTPS. Default is false. + - apiKey: An optional API key for authentication. If the API Endpoint is enabled as private, a Service Account API key will be required. + - customUserAgent: An optional custom user agent string for network requests. + - skipAdvertisingIdDetection: Boolean flag to skip the detection of advertising IDs. Default is false. + */ public init( tenant: String, originSlug: String, @@ -54,7 +85,7 @@ public class OptableConfig: NSObject { self.skipAdvertisingIdDetection = skipAdvertisingIdDetection } - func edgeURL(_ endpoint: String) -> URL? { + func buildEdgeURL(_ endpoint: String) -> URL? { var components = URLComponents() components.scheme = insecure ? "http" : "https" components.host = host diff --git a/Source/OptableIdentifier.swift b/Source/OptableIdentifier.swift new file mode 100644 index 0000000..f19c711 --- /dev/null +++ b/Source/OptableIdentifier.swift @@ -0,0 +1,86 @@ +// +// OptableIdentifier.swift +// OptableSDK +// +// Created by user on 16.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +import Foundation + +public enum OptableIdentifier: RawRepresentable, Hashable { + // Personal identifiers + case emailAddress // e + case phoneNumber // p + case postalCode // z + + // IP addresses + case ipv4Address // i4 + case ipv6Address // i6 + + // Device IDs + case appleIDFA // a + case googleGAID // g + case rokuRIDA // r + case samsungTIFA // s + case amazonFireAFAI // f + + // Universal / identity frameworks + case netID // n + case id5 // id5 + case utiq // utiq + + // Custom IDs (c, c1...cN) + case custom(Int?) // nil = "c", 1..N = "c1"..."cN" + + // Optable VID + case optableVID // v + + public init?(rawValue: String) { + switch rawValue { + case "e": self = .emailAddress + case "p": self = .phoneNumber + case "z": self = .postalCode + case "i4": self = .ipv4Address + case "i6": self = .ipv6Address + case "a": self = .appleIDFA + case "g": self = .googleGAID + case "r": self = .rokuRIDA + case "s": self = .samsungTIFA + case "f": self = .amazonFireAFAI + case "n": self = .netID + case "id5": self = .id5 + case "utiq": self = .utiq + case "c": self = .custom(nil) + case "v": self = .optableVID + default: + if rawValue.starts(with: "c"), + let number = Int(rawValue.dropFirst()) { + self = .custom(number) + } else { + return nil + } + } + } + + public var rawValue: String { + switch self { + case .emailAddress: return "e" + case .phoneNumber: return "p" + case .postalCode: return "z" + case .ipv4Address: return "i4" + case .ipv6Address: return "i6" + case .appleIDFA: return "a" + case .googleGAID: return "g" + case .rokuRIDA: return "r" + case .samsungTIFA: return "s" + case .amazonFireAFAI: return "f" + case .netID: return "n" + case .id5: return "id5" + case .utiq: return "utiq" + case .custom(nil): return "c" + case let .custom(n?): return "c\(n)" + case .optableVID: return "v" + } + } +} diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index 0f0d3f8..eab3f87 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -6,28 +6,21 @@ // See LICENSE for details. // -import CommonCrypto import Foundation -#if canImport(CryptoKit) - import CryptoKit -#endif -import AdSupport -import AppTrackingTransparency // MARK: - OptableDelegate -/// -/// OptableDelegate is a delegate protocol that the caller may optionally use. Swift applications can choose to integrate using -/// callbacks or the delegator pattern, whereas Objective-C apps must use the delegator pattern. -/// -/// The OptableDelegate protocol consists of implementing *Ok() and *Err() event handlers. The *Ok() handler will -/// receive an NSDictionary when the delegate variant of the targeting() API is called, and an HTTPURLResponse in all other -/// SDK APIs that do not return actual data on success (e.g., identify(), witness(), etc.) -/// -/// The *Err() handlers will be called with an NSError instance on SDK API errors. -/// -/// Finally note that for the delegate variant of SDK API methods, internal exceptions will result in setting the NSError -/// object passed which is passed by reference to the method, and not calling the delegate. -/// +/** + OptableDelegate is a delegate protocol that the caller may optionally use. + Swift applications can choose to integrate using callbacks or the delegator pattern, whereas Objective-C apps must use the delegator pattern. + + The OptableDelegate protocol consists of implementing *Ok() and *Err() event handlers. + The *Ok() handler will receive an NSDictionary when the delegate variant of the targeting() API is called, + and an HTTPURLResponse in all other SDK APIs that do not return actual data on success (e.g., identify(), witness(), etc.) + + The *Err() handlers will be called with an NSError instance on SDK API errors. + + Finally note that for the delegate variant of SDK API methods, internal exceptions will result in setting the NSError object passed which is passed by reference to the method, and not calling the delegate. + */ @objc public protocol OptableDelegate { func identifyOk(_ result: HTTPURLResponse) @@ -41,44 +34,33 @@ public protocol OptableDelegate { } // MARK: - OptableSDK -/// -/// OptableSDK exposes an API that is used by an iOS app developer integrating with an Optable Sandbox. -/// -/// An instance of OptableSDK refers to an Optable Sandbox specified by the caller via `host` and `app` arguments provided to the constructor. -/// -/// It is possible to create multiple instances of OptableSDK, should the developer want to integrate with multiple Sandboxes. -/// -/// The OptableSDK keeps some state in UserDefaults (https:///developer.apple.com/documentation/foundation/userdefaults), a key/value store persisted -/// across launches of the app. The state is therefore unique to the app+device, and not globally unique to the app across devices. -/// +/** + OptableSDK exposes an API that is used by an iOS app developer integrating with an Optable Sandbox. + + An instance of OptableSDK refers to an Optable Sandbox specified by the caller via `host` and `app` arguments provided to the constructor. + + It is possible to create multiple instances of OptableSDK, should the developer want to integrate with multiple Sandboxes. + + The OptableSDK keeps some state in UserDefaults (https:///developer.apple.com/documentation/foundation/userdefaults), a key/value store persisted across launches of the app. The state is therefore unique to the app+device, and not globally unique to the app across devices. + */ @objc public class OptableSDK: NSObject { - @objc public var delegate: OptableDelegate? - - public enum OptableError: Error { - case identify(String) - case profile(String) - case targeting(String) - case witness(String) - } + @objc + public var delegate: OptableDelegate? - var config: OptableConfig - var api: EdgeAPI + let config: OptableConfig + let api: EdgeAPI + let identifierEncoder: OptableIdentifierEncoder - /// - /// `OptableSDK` returns an instance of the SDK configured to talk to the sandbox specified by `OptableConfig`: - /// + /// `OptableSDK` returns an instance of the SDK configured to use the sandbox specified by `OptableConfig`: @objc public init(config: OptableConfig) { self.config = config self.api = EdgeAPI(config) + self.identifierEncoder = OptableIdentifierEncoder() } - /// - /// OptableSDK.version returns the SDK version as a String. The version is based on the short - /// version string set in the SDK project CFBundleShortVersionString. When the SDK is included via - /// Cocoapods, it will be set automatically on `pod install` according to the podspec version. - /// + /// OptableSDK version static var version: String { let sdkBundle = Bundle(for: OptableSDK.self) @@ -91,15 +73,268 @@ public class OptableSDK: NSObject { } } -// MARK: - Interface +// MARK: - Identify +public extension OptableSDK { + /** + identify(ids, completion) issues a call to the Optable Sandbox "identify" API, passing the specified list of type-prefixed IDs. + + 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([ + "e": "example@example.com", + "p": "1234567890", + ], completion) + ``` + */ + func identify(_ ids: [String: String], _ completion: @escaping (Result) -> Void) throws { + try _identify(OptableIdentifiers(ids), completion: completion) + } + + /** + This is the swift-friendly version of the `identify(ids, completion)` API. + + You can use predefined `OptableIdentifier` enum easily provide keys. + ```swift + // Example + optableSDK.identify([ + OptableIdentifier.emailAddress: "example@example.com", + OptableIdentifier.phoneNumber: "1234567890", + ], completion) + ``` + */ + func identify(_ ids: [OptableIdentifier: String], _ completion: @escaping (Result) -> Void) throws { + try _identify(OptableIdentifiers(ids), completion: completion) + } + + // MARK: Async/Await support + + /** + This is the Swift Concurrency compatible version of the `identify(ids, completion)` API. + + Instead of completion callbacks, function have to be awaited. + */ + @available(iOS 13.0, *) + func identify(_ ids: [String: String]) async throws -> HTTPURLResponse { + return try await withCheckedThrowingContinuation({ [unowned self] continuation in + do { + try self._identify(OptableIdentifiers(ids), completion: { continuation.resume(with: $0) }) + } catch { + continuation.resume(throwing: error) + } + }) + } + + /** + This is the Swift Concurrency compatible version of the `identify(ids, completion)` API. + + Instead of completion callbacks, function have to be awaited. + */ + @available(iOS 13.0, *) + func identify(_ ids: [OptableIdentifier: String]) async throws -> HTTPURLResponse { + return try await withCheckedThrowingContinuation({ [unowned self] continuation in + do { + try self._identify(OptableIdentifiers(ids), completion: { continuation.resume(with: $0) }) + } catch { + continuation.resume(throwing: error) + } + }) + } + + // MARK: Objective-C support + /** + This is the Objective-C compatible version of the `identify(ids, completion)` API. + + Instead of completion callbacks, delegate methods are called. + */ + @objc + func identify(_ ids: [String: String]) throws { + try self._identify(OptableIdentifiers(ids)) { result in + switch result { + case let .success(response): + self.delegate?.identifyOk(response) + case let .failure(error as NSError): + self.delegate?.identifyErr(error) + } + } + } +} + +// MARK: - Targeting +public extension OptableSDK { + /** + targeting(completion) calls the Optable Sandbox "targeting" API, which returns the key-value targeting data matching the user/device/app. + + The targeting method is asynchronous, and on completion it will call the specified completion handler, + passing it either the NSDictionary targeting data on success, or an NSError on failure. + + On success, this method will also cache the resulting targeting data in client storage, which can + be access using targetingFromCache(), and cleared using targetingClearCache(). + */ + func targeting(completion: @escaping (Result) -> Void) throws { + try _targeting(completion: completion) + } + + /// targetingFromCache() returns the previously cached targeting data, if any. + @objc + func targetingFromCache() -> NSDictionary? { + guard let keyvalues = self.api.storage.getTargeting() as NSDictionary? else { + return nil + } + return keyvalues + } + + /// targetingClearCache() clears any previously cached targeting data. + @objc + func targetingClearCache() { + self.api.storage.clearTargeting() + } + + // MARK: Async/Await support + /** + This is the Swift Concurrency compatible version of the `targeting(completion)` API. + + Instead of completion callbacks, function have to be awaited. + */ + @available(iOS 13.0, *) + func targeting() async throws -> NSDictionary { + return try await withCheckedThrowingContinuation({ [unowned self] continuation in + do { + try self._targeting(completion: { continuation.resume(with: $0) }) + } catch { + continuation.resume(throwing: error) + } + }) + } + + // MARK: Objective-C support + /** + This is the Objective-C compatible version of the `targeting(completion)` API. + + Instead of completion callbacks, delegate methods are called. + */ + @objc + func targeting() throws { + try self._targeting { result in + switch result { + case let .success(keyvalues): + self.delegate?.targetingOk(keyvalues) + case let .failure(error as NSError): + self.delegate?.targetingErr(error) + } + } + } +} + +// MARK: - Witness +public extension OptableSDK { + /** + witness(event, properties, completion) calls the Optable Sandbox "witness" API in order to log a specified 'event' (e.g., "app.screenView", "ui.buttonPressed"), with the specified keyvalue NSDictionary 'properties', which can be subsequently used for audience assembly. + + 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 { + try _witness(event: event, properties: properties, completion: completion) + } + + // MARK: Async/Await support + /** + This is the Swift Concurrency compatible version of the `witness(event, properties, completion)` API. + + Instead of completion callbacks, function have to be awaited. + */ + @available(iOS 13.0, *) + func witness(event: String, properties: NSDictionary) async throws -> HTTPURLResponse { + return try await withCheckedThrowingContinuation({ [unowned self] continuation in + do { + try self._witness(event: event, properties: properties, completion: { continuation.resume(with: $0) }) + } catch { + continuation.resume(throwing: error) + } + }) + } + + // MARK: Objective-C support + /** + This is the Objective-C compatible version of the `witness(event, properties, completion)` API. + + Instead of completion callbacks, delegate methods are called. + */ + @objc + func witness(_ event: String, properties: NSDictionary) throws { + try self.witness(event: event, properties: properties) { result in + switch result { + case let .success(response): + self.delegate?.witnessOk(response) + case let .failure(error as NSError): + self.delegate?.witnessErr(error) + } + } + } +} + +// MARK: - Profile public extension OptableSDK { - /// - /// identify(ids, completion) issues a call to the Optable Sandbox "identify" API, passing the specified - /// list of type-prefixed IDs. It is asynchronous, and on completion it will call the specified completion handler, passing - /// it either the HTTPURLResponse on success, or an OptableError on failure. - /// - func identify(ids: [String], _ completion: @escaping (Result) -> Void) throws { - try api.identify(ids: ids) { data, response, error in + /** + profile(traits, completion) calls the Optable Sandbox "profile" API in order to associate specified 'traits' (i.e., key-value pairs) with the user's device. + + 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, _ completion: @escaping (Result) -> Void) throws { + try _profile(traits: traits, completion: completion) + } + + // MARK: Async/Await support + /** + This is the Swift Concurrency compatible version of the `profile(traits, completion)` API. + + Instead of completion callbacks, function have to be awaited. + */ + @available(iOS 13.0, *) + func profile(traits: NSDictionary) async throws -> HTTPURLResponse { + return try await withCheckedThrowingContinuation({ [unowned self] continuation in + do { + try self._profile(traits: traits, completion: { continuation.resume(with: $0) }) + } catch { + continuation.resume(throwing: error) + } + }) + } + + // MARK: Objective-C support + /** + This is the Objective-C compatible version of the `profile(traits, completion)` API. + + Instead of completion callbacks, delegate methods are called. + */ + @objc + func profile(traits: NSDictionary) throws { + try _profile(traits: traits, completion: { result in + switch result { + case let .success(response): + self.delegate?.profileOk(response) + case let .failure(error as NSError): + self.delegate?.profileErr(error) + } + }) + } +} + +// MARK: - Private +private extension OptableSDK { + private func _identify(_ ids: OptableIdentifiers, completion: @escaping (Result) -> Void) throws { + var ids = ids + + if config.skipAdvertisingIdDetection == false, + ATT.adfaAvailable, + ATT.advertisingIdentifier != UUID(uuid: uuid_t(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) { + ids[.appleIDFA] = ATT.advertisingIdentifier.uuidString + } + + try api.identify(ids: ids, completionHandler: { data, response, error in guard let response = response as? HTTPURLResponse, error == nil, data != nil else { if let err = error { completion(.failure(OptableError.identify("Session error: \(err)"))) @@ -108,7 +343,7 @@ public extension OptableSDK { } return } - guard 200 ..< 300 ~= response.statusCode else { + guard case .successful = HTTPStatusCode(statusCode: response.statusCode) else { var msg = "HTTP response.statusCode: \(response.statusCode)" do { let json = try JSONSerialization.jsonObject(with: data ?? Data(), options: []) @@ -118,56 +353,10 @@ public extension OptableSDK { return } completion(.success(response)) - }?.resume() + })?.resume() } - /// - /// identify(email, aaid, ppid, completion) issues a call to the Optable Sandbox "identify" API, passing it the SHA-256 - /// of the caller-provided 'email' and, when specified via the 'aaid' Boolean, the Apple ID For Advertising (IDFA) - /// associated with the device. When 'ppid' is provided as a string, it is also sent for identity resolution. - /// - /// The identify method is asynchronous, and on completion it will call the specified completion handler, passing - /// it either the HTTPURLResponse on success, or an OptableError on failure. - /// - func identify(email: String, aaid: Bool = false, ppid: String = "", _ completion: @escaping (Result) -> Void) throws { - var ids = [String]() - - if email != "" { - ids.append(self.eid(email)) - } - - if aaid { - if #available(iOS 14, *) { - ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in - if status == .authorized { - ids.append(self.aaid(ASIdentifierManager.shared().advertisingIdentifier.uuidString)) - } - }) - } else { - if ASIdentifierManager.shared().isAdvertisingTrackingEnabled { - ids.append(self.aaid(ASIdentifierManager.shared().advertisingIdentifier.uuidString)) - } - } - } - - if !ppid.isEmpty { - ids.append(self.cid(ppid)) - } - - try self.identify(ids: ids, completion) - } - - /// - /// targeting(completion) calls the Optable Sandbox "targeting" API, which returns the key-value targeting - /// data matching the user/device/app. - /// - /// The targeting method is asynchronous, and on completion it will call the specified completion handler, - /// passing it either the NSDictionary targeting data on success, or an OptableError on failure. - /// - /// On success, this method will also cache the resulting targeting data in client storage, which can - /// be access using targetingFromCache(), and cleared using targetingClearCache(). - /// - func targeting(_ completion: @escaping (Result) -> Void) throws { + private func _targeting(completion: @escaping (Result) -> Void) throws { try api.targeting { data, response, error in guard let response = response as? HTTPURLResponse, error == nil, data != nil else { if let err = error { @@ -201,34 +390,7 @@ public extension OptableSDK { }?.resume() } - /// - /// targetingFromCache() returns the previously cached targeting data, if any. - /// - @objc - func targetingFromCache() -> NSDictionary? { - guard let keyvalues = self.api.storage.getTargeting() as NSDictionary? else { - return nil - } - return keyvalues - } - - /// - /// targetingClearCache() clears any previously cached targeting data. - /// - @objc - func targetingClearCache() { - self.api.storage.clearTargeting() - } - - /// - /// witness(event, properties, completion) calls the Optable Sandbox "witness" API in order to log - /// a specified 'event' (e.g., "app.screenView", "ui.buttonPressed"), with the specified keyvalue - /// NSDictionary 'properties', which can be subsequently used for audience assembly. - /// - /// The witness method is asynchronous, and on completion it will call the specified completion handler, - /// passing it either the HTTPURLResponse on success, or an OptableError 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 api.witness(event: event, properties: properties) { data, response, error in guard let response = response as? HTTPURLResponse, error == nil else { if let err = error { @@ -251,15 +413,7 @@ public extension OptableSDK { }?.resume() } - /// - /// profile(traits, completion) calls the Optable Sandbox "profile" API in order to associate - /// specified 'traits' (i.e., key-value pairs) with the user's device. 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 OptableError on failure. - /// - func profile(traits: NSDictionary, _ completion: @escaping (Result) -> Void) throws { + func _profile(traits: NSDictionary, completion: @escaping (Result) -> Void) throws { try api.profile(traits: traits) { data, response, error in guard let response = response as? HTTPURLResponse, error == nil else { if let err = error { @@ -281,204 +435,4 @@ public extension OptableSDK { completion(.success(response)) }?.resume() } - - /// - /// eid(email) is a helper that returns type-prefixed SHA256(downcase(email)) - /// - @objc - func eid(_ email: String) -> String { - let pfx = "e:" - let normEmail = Data(email.lowercased().trimmingCharacters(in: .whitespacesAndNewlines).utf8) - - #if canImport(CryptoKit) - if #available(iOS 13.0, *) { - return pfx + SHA256.hash(data: normEmail).compactMap { - String(format: "%02x", $0) - }.joined() - } else { - return pfx + self.cchash(normEmail) - } - #else - return pfx + self.cchash(normEmail) - #endif - } - - @objc - private func cchash(_ input: Data) -> String { - var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) - input.withUnsafeBytes { bytes in - _ = CC_SHA256(bytes.baseAddress, CC_LONG(input.count), &digest) - } - return digest.makeIterator().compactMap { - String(format: "%02x", $0) - }.joined() - } - - /// - /// aaid(idfa) is a helper that returns the type-prefixed Apple ID For Advertising - /// - @objc - func aaid(_ idfa: String) -> String { - return "a:" + idfa.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) - } - - /// - /// cid(ppid) is a helper that returns custom type-prefixed origin-provided PPID - /// - @objc - func cid(_ ppid: String) -> String { - return "c:" + ppid.trimmingCharacters(in: .whitespacesAndNewlines) - } - - /// - /// eidFromURL(urlString) is a helper that returns a type-prefixed ID based on - /// the query string oeid=sha256value parameter in the specified urlString, if - /// one is found. Otherwise, it returns an empty string. - /// - /// The use for this is when handling incoming universal links which might - /// contain an "oeid" value with the SHA256(downcase(email)) of a user, such as - /// encoded links in newsletter Emails sent by the application developer. Such - /// hashed Email values can be used in calls to identify() - /// - @objc - func eidFromURL(_ urlString: String) -> String { - guard let url = URL(string: urlString) else { return "" } - guard let urlc = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return "" } - guard let urlqis = urlc.queryItems else { return "" } - - /// Look for an oeid parameter in the urlString: - var oeid = "" - for qi: URLQueryItem in urlqis { - guard let val = qi.value else { - continue - } - if qi.name.lowercased() == "oeid" { - oeid = val - break - } - } - - /// Check that oeid looks like a valid SHA256: - let range = NSRange(location: 0, length: oeid.utf16.count) - guard let regex = try? NSRegularExpression(pattern: "[a-f0-9]{64}", options: .caseInsensitive) else { return "" } - if (oeid.count != 64) || (regex.firstMatch(in: oeid, options: [], range: range) == nil) { - return "" - } - - return "e:" + oeid.lowercased() - } - - /// - /// tryIdentifyFromURL(urlString) is a helper that attempts to find a valid-looking - /// "oeid" parameter in the specified urlString's query string parameters and, if found, - /// calls self.identify([oeid]). - /// - /// The use for this is when handling incoming universal links which might contain an - /// "oeid" value with the SHA256(downcase(email)) of an incoming user, such as encoded - /// links in newsletter Emails sent by the application developer. - /// - @objc - func tryIdentifyFromURL(_ urlString: String) throws { - let oeid = self.eidFromURL(urlString) - - if !oeid.isEmpty { - try self.identify(ids: [oeid]) { _ in /* no-op */ } - } - } -} - -// MARK: - ObjectiveC support -public extension OptableSDK { - /// - /// identify(ids) is the "delegate variant" of the identify(ids, completion) method. It wraps the latter with - /// a delegator callback. - /// - /// This is the Objective-C compatible version of the identify(ids, completion) API. - /// - @objc - func identify(_ ids: [String]) throws { - try self.identify(ids: ids) { result in - switch result { - case let .success(response): - self.delegate?.identifyOk(response) - case let .failure(error as NSError): - self.delegate?.identifyErr(error) - } - } - } - - /// - /// identify(email, aaid, ppid) is the "delegate variant" of the identify(email, aaid, ppid, completion) method. - /// It wraps the latter with a delegator callback. - /// - /// This is the Objective-C compatible version of the identify(email, aaid, ppid, completion) API. - /// - @objc - func identify(_ email: String, aaid: Bool = false, ppid: String = "") throws { - try self.identify(email: email, aaid: aaid, ppid: ppid) { result in - switch result { - case let .success(response): - self.delegate?.identifyOk(response) - case let .failure(error as NSError): - self.delegate?.identifyErr(error) - } - } - } - - /// - /// targeting() is the "delegate variant" of the targeting(completion) method. It wraps the latter with - /// a delegator callback. - /// - /// This is the Objective-C compatible version of the targeting(completion) API. - /// - @objc - func targeting() throws { - try self.targeting { result in - switch result { - case let .success(keyvalues): - self.delegate?.targetingOk(keyvalues) - case let .failure(error as NSError): - self.delegate?.targetingErr(error) - } - } - } - - /// - /// witness(event, properties) is the "delegate variant" of the witness(event, properties, completion) method. - /// It wraps the latter with a delegator callback. - /// - /// This is the Objective-C compatible version of the witness(event, properties, completion) API. - /// - @objc - func witness(_ event: String, properties: NSDictionary) throws { - try self.witness(event: event, properties: properties) { result in - switch result { - case let .success(response): - self.delegate?.witnessOk(response) - case let .failure(error as NSError): - self.delegate?.witnessErr(error) - } - } - } - - /// - /// profile(traits) is the "delegate variant" of the profile(traits, completion) method. - /// It wraps the latter with a delegator callback. - /// - /// This is the Objective-C compatible version of the profile(traits, completion) API. - /// - @objc - func profile(traits: NSDictionary) throws { - try self.profile(traits: traits) { result in - switch result { - case let .success(response): - self.delegate?.profileOk(response) - case let .failure(error as NSError): - self.delegate?.profileErr(error) - } - } - } } - -// MARK: - Swift Concurrency support -public extension OptableSDK { /* TODO: */ } From 79b5d08c849a5bc88a7a5dadcaf9a1c49ea56249 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Tue, 16 Dec 2025 21:07:12 +0100 Subject: [PATCH 10/42] - Updated code-doc --- Source/Core/EdgeAPI.swift | 7 +++++++ Source/OptableIdentifier.swift | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index ae60765..34e066f 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -9,6 +9,13 @@ import Foundation import WebKit +/** + Real Time API + + For more info check: + [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide) + + */ final class EdgeAPI { private let kPassportHeader: String = "X-Optable-Visitor" diff --git a/Source/OptableIdentifier.swift b/Source/OptableIdentifier.swift index f19c711..a488f9a 100644 --- a/Source/OptableIdentifier.swift +++ b/Source/OptableIdentifier.swift @@ -8,6 +8,13 @@ import Foundation +/** + Optable Identifier Types + + For more info check: + [](https://docs.optable.co/optable-documentation/getting-started/reference/identifier-types) + + */ public enum OptableIdentifier: RawRepresentable, Hashable { // Personal identifiers case emailAddress // e From 7b98996b1a7a401d34178f946212fb4fabd0985d Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Wed, 17 Dec 2025 12:12:54 +0100 Subject: [PATCH 11/42] - Added basic OptableIdentifierEncoder --- Source/Core/OptableIdentifierEncoder.swift | 171 ++++++++++++++------- 1 file changed, 114 insertions(+), 57 deletions(-) diff --git a/Source/Core/OptableIdentifierEncoder.swift b/Source/Core/OptableIdentifierEncoder.swift index 9542610..ce39ce9 100644 --- a/Source/Core/OptableIdentifierEncoder.swift +++ b/Source/Core/OptableIdentifierEncoder.swift @@ -15,52 +15,105 @@ import Foundation // MARK: - OptableIdentifierEncoder @objc final class OptableIdentifierEncoder: NSObject { - /// - /// aaid(idfa) is a helper that returns the type-prefixed Apple ID For Advertising - /// - @objc - func aaid(_ idfa: String) -> String { - return "a:" + idfa.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + /// Builds Enriched Identifier from Email address + func email(_ email: String) -> String { + let prefix = OptableIdentifier.emailAddress.rawValue + let normalizedData = Data(email.lowercased().trimmingCharacters(in: .whitespacesAndNewlines).utf8) + let identifier = sha256(data: normalizedData) + return "\(prefix):\(identifier)" } - /// - /// cid(ppid) is a helper that returns custom type-prefixed origin-provided PPID - /// - @objc - func cid(_ ppid: String) -> String { - return "c:" + ppid.trimmingCharacters(in: .whitespacesAndNewlines) + /// Builds Enriched Identifier from Phone number + func phoneNumber(_ phoneNumber: String) -> String { + let prefix = OptableIdentifier.phoneNumber.rawValue + let normalizedData = Data(phoneNumber.lowercased().trimmingCharacters(in: .whitespacesAndNewlines).utf8) + let identifier = sha256(data: normalizedData) + return "\(prefix):\(identifier)" } - /// - /// eid(email) is a helper that returns type-prefixed SHA256(downcase(email)) - /// - @objc - func eid(_ email: String) -> String { - let pfx = "e:" - let normEmail = Data(email.lowercased().trimmingCharacters(in: .whitespacesAndNewlines).utf8) + /// Builds Enriched Identifier from Postal code + func postalCode(_ postalCode: String) -> String { + let prefix = OptableIdentifier.postalCode.rawValue + let identifier = postalCode.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" + } - #if canImport(CryptoKit) - if #available(iOS 13.0, *) { - return pfx + SHA256.hash(data: normEmail).compactMap { - String(format: "%02x", $0) - }.joined() - } else { - return pfx + self.cchash(normEmail) - } - #else - return pfx + self.cchash(normEmail) - #endif + /// Builds Enriched Identifier from IPv4 address + func ipv4(_ ipv4: String) -> String { + let prefix = OptableIdentifier.ipv4Address.rawValue + let identifier = ipv4.trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" + } + + /// Builds Enriched Identifier from IPv6 address + func ipv6(_ ipv6: String) -> String { + let prefix = OptableIdentifier.ipv6Address.rawValue + let identifier = ipv6.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" + } + + /// Builds Enriched Identifier from Apple IDFA + func idfa(_ idfa: String) -> String { + let prefix = OptableIdentifier.appleIDFA.rawValue + let identifier = idfa.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" + } + + /// Builds Enriched Identifier from Google GAID + func gaid(_ gaid: String) -> String { + let prefix = OptableIdentifier.googleGAID.rawValue + let identifier = gaid.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" + } + + /// Builds Enriched Identifier from Roku RIDA + func rida(_ rida: String) -> String { + let prefix = OptableIdentifier.rokuRIDA.rawValue + let identifier = rida.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" + } + + /// Builds Enriched Identifier from Samsung TV TIFA + func tifa(_ tifa: String) -> String { + let prefix = OptableIdentifier.samsungTIFA.rawValue + let identifier = tifa.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" + } + + /// Builds Enriched Identifier from Amazon Fire AFAI + func afai(_ afai: String) -> String { + let prefix = OptableIdentifier.amazonFireAFAI.rawValue + let identifier = afai.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" + } + + /// Builds Enriched Identifier from NetID + func netid(_ netid: String) -> String { + let prefix = OptableIdentifier.netID.rawValue + let identifier = netid.trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" + } + + /// Builds Enriched Identifier from ID5 + func id5(_ id5: String) -> String { + let prefix = OptableIdentifier.id5.rawValue + let identifier = id5.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" } + /// Builds Enriched Identifier from Utiq + func utiq(_ utiq: String) -> String { + let prefix = OptableIdentifier.utiq.rawValue + let identifier = utiq.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" + } + + /// Builds Enriched Identifier from Custom Publisher Provided ID (PPID) @objc - func cchash(_ input: Data) -> String { - var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) - input.withUnsafeBytes { bytes in - _ = CC_SHA256(bytes.baseAddress, CC_LONG(input.count), &digest) - } - return digest.makeIterator().compactMap { - String(format: "%02x", $0) - }.joined() + func custom(_ idx: Int = 0, _ ppid: String) -> String { + let prefix = OptableIdentifier.custom(idx).rawValue + let identifier = ppid.trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" } /// @@ -73,7 +126,6 @@ final class OptableIdentifierEncoder: NSObject { /// encoded links in newsletter Emails sent by the application developer. Such /// hashed Email values can be used in calls to identify() /// - @objc func eidFromURL(_ urlString: String) -> String { guard let url = URL(string: urlString) else { return "" } guard let urlc = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return "" } @@ -101,22 +153,27 @@ final class OptableIdentifierEncoder: NSObject { return "e:" + oeid.lowercased() } - /// - /// tryIdentifyFromURL(urlString) is a helper that attempts to find a valid-looking - /// "oeid" parameter in the specified urlString's query string parameters and, if found, - /// calls self.identify([oeid]). - /// - /// The use for this is when handling incoming universal links which might contain an - /// "oeid" value with the SHA256(downcase(email)) of an incoming user, such as encoded - /// links in newsletter Emails sent by the application developer. - /// - // TODO: to remove -// @objc -// func tryIdentifyFromURL(_ urlString: String) throws { -// let oeid = self.eidFromURL(urlString) -// -// if !oeid.isEmpty { -// try self.identify(ids: [oeid]) { _ in /* no-op */ } -// } -// } + // MARK: - Private + private func sha256(data: Data) -> String { + #if canImport(CryptoKit) + if #available(iOS 13.0, *) { + return SHA256 + .hash(data: data) + .compactMap({ String(format: "%02x", $0) }) + .joined() + } + #endif + + return self.cchash(data) + } + + private func cchash(_ input: Data) -> String { + var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + input.withUnsafeBytes { bytes in + _ = CC_SHA256(bytes.baseAddress, CC_LONG(input.count), &digest) + } + return digest.makeIterator().compactMap { + String(format: "%02x", $0) + }.joined() + } } From 373aece2cb4fdc179529a24a1431c232ceecc12f Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Wed, 17 Dec 2025 12:13:32 +0100 Subject: [PATCH 12/42] - Cleaned up OptableConfig --- Source/Core/EdgeAPI.swift | 30 +++++++++++++++++++++++------- Source/OptableConfig.swift | 13 ------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index 34e066f..2baaf64 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -9,12 +9,13 @@ import Foundation import WebKit +// MARK: - EdgeAPI /** Real Time API - + For more info check: [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide) - + */ final class EdgeAPI { private let kPassportHeader: String = "X-Optable-Visitor" @@ -40,31 +41,33 @@ final class EdgeAPI { // MARK: Endpoints func identify(ids: OptableIdentifiers, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.buildEdgeURL("identify") else { return nil } + guard let url = buildEdgeAPIURL(endpoint:"identify") else { return nil } let jsonData = try jsonEncoder.encode(ids) let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: jsonData) return dispatchRequest(req, completionHandler) } func profile(traits: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.buildEdgeURL("profile") else { return nil } + guard let url = buildEdgeAPIURL(endpoint:"profile") else { return nil } let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), obj: ["traits": traits]) return dispatchRequest(req, completionHandler) } func targeting(completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.buildEdgeURL("targeting") else { return nil } + guard let url = buildEdgeAPIURL(endpoint:"targeting") else { return nil } let req = try buildRequest(.GET, url: url, headers: resolveHeaders()) return dispatchRequest(req, completionHandler) } func witness(event: String, properties: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = config.buildEdgeURL("witness") else { return nil } + guard let url = buildEdgeAPIURL(endpoint:"witness") else { return nil } let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), obj: ["event": event, "properties": properties]) return dispatchRequest(req, completionHandler) } +} - // MARK: Private +// MARK: - Private +extension EdgeAPI { private func dispatchRequest(_ req: URLRequest, _ completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { return URLSession.shared.dataTask(with: req) { data, response, error in guard let res = response as? HTTPURLResponse, error == nil else { @@ -173,4 +176,17 @@ final class EdgeAPI { return request } + + func buildEdgeAPIURL(endpoint: String) -> URL? { + var components = URLComponents() + components.scheme = config.insecure ? "http" : "https" + components.host = config.host + components.path = "/\(config.path)/\(endpoint)" + components.queryItems = [ + .init(name: "t", value: config.tenant.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), + .init(name: "o", value: config.originSlug.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), + .init(name: "osdk", value: OptableSDK.version.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), + ] + return components.url + } } diff --git a/Source/OptableConfig.swift b/Source/OptableConfig.swift index fa44be0..9acc5e2 100644 --- a/Source/OptableConfig.swift +++ b/Source/OptableConfig.swift @@ -84,17 +84,4 @@ public class OptableConfig: NSObject { self.customUserAgent = customUserAgent self.skipAdvertisingIdDetection = skipAdvertisingIdDetection } - - func buildEdgeURL(_ endpoint: String) -> URL? { - var components = URLComponents() - components.scheme = insecure ? "http" : "https" - components.host = host - components.path = "/\(path)/\(endpoint)" - components.queryItems = [ - .init(name: "t", value: tenant.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), - .init(name: "o", value: originSlug.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), - .init(name: "osdk", value: OptableSDK.version.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), - ] - return components.url - } } From 429c60cfdb768168c93f3d6d38c26f5e8e7a50e2 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Wed, 17 Dec 2025 12:13:46 +0100 Subject: [PATCH 13/42] - Updated OptableSDK code-doc --- Source/OptableSDK.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index eab3f87..f135583 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -99,8 +99,8 @@ public extension OptableSDK { ```swift // Example optableSDK.identify([ - OptableIdentifier.emailAddress: "example@example.com", - OptableIdentifier.phoneNumber: "1234567890", + .emailAddress: "example@example.com", + .phoneNumber: "1234567890", ], completion) ``` */ From f70b0b2891f09b35a201f9f281e93d8e63e5dcbb Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Wed, 17 Dec 2025 14:31:27 +0100 Subject: [PATCH 14/42] - Cleaned up OptableSDK --- Source/OptableSDK.swift | 62 +++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index f135583..386a2c9 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -50,14 +50,12 @@ public class OptableSDK: NSObject { let config: OptableConfig let api: EdgeAPI - let identifierEncoder: OptableIdentifierEncoder /// `OptableSDK` returns an instance of the SDK configured to use the sandbox specified by `OptableConfig`: @objc public init(config: OptableConfig) { self.config = config self.api = EdgeAPI(config) - self.identifierEncoder = OptableIdentifierEncoder() } /// OptableSDK version @@ -109,7 +107,6 @@ public extension OptableSDK { } // MARK: Async/Await support - /** This is the Swift Concurrency compatible version of the `identify(ids, completion)` API. @@ -344,12 +341,8 @@ private extension OptableSDK { return } guard case .successful = HTTPStatusCode(statusCode: response.statusCode) else { - var msg = "HTTP response.statusCode: \(response.statusCode)" - do { - let json = try JSONSerialization.jsonObject(with: data ?? Data(), options: []) - msg += ", data: \(json)" - } catch {} - completion(.failure(OptableError.identify(msg))) + let errDesc = OptableSDK.generateEdgeAPIErrorDescription(with: data, response: response) + completion(.failure(OptableError.identify(errDesc, code: response.statusCode))) return } completion(.success(response)) @@ -357,7 +350,7 @@ private extension OptableSDK { } private func _targeting(completion: @escaping (Result) -> Void) throws { - try api.targeting { data, response, error in + try api.targeting(completionHandler: { data, response, error in guard let response = response as? HTTPURLResponse, error == nil, data != nil else { if let err = error { completion(.failure(OptableError.targeting("Session error: \(err)"))) @@ -366,13 +359,9 @@ private extension OptableSDK { } return } - guard 200 ..< 300 ~= response.statusCode else { - var msg = "HTTP response.statusCode: \(response.statusCode)" - do { - let json = try JSONSerialization.jsonObject(with: data ?? Data(), options: []) - msg += ", data: \(json)" - } catch {} - completion(.failure(OptableError.targeting(msg))) + guard case .successful = HTTPStatusCode(statusCode: response.statusCode) else { + let errDesc = OptableSDK.generateEdgeAPIErrorDescription(with: data, response: response) + completion(.failure(OptableError.targeting(errDesc, code: response.statusCode))) return } @@ -387,11 +376,11 @@ private extension OptableSDK { } catch { completion(.failure(OptableError.targeting("Error parsing JSON response: \(error)"))) } - }?.resume() + })?.resume() } func _witness(event: String, properties: NSDictionary, completion: @escaping (Result) -> Void) throws { - try api.witness(event: event, properties: properties) { data, response, error in + try api.witness(event: event, properties: properties, completionHandler: { data, response, error in guard let response = response as? HTTPURLResponse, error == nil else { if let err = error { completion(.failure(OptableError.witness("Session error: \(err)"))) @@ -400,21 +389,17 @@ private extension OptableSDK { } return } - guard 200 ..< 300 ~= response.statusCode else { - var msg = "HTTP response.statusCode: \(response.statusCode)" - do { - let json = try JSONSerialization.jsonObject(with: data ?? Data(), options: []) - msg += ", data: \(json)" - } catch {} - completion(.failure(OptableError.witness(msg))) + guard case .successful = HTTPStatusCode(statusCode: response.statusCode) else { + let errDesc = OptableSDK.generateEdgeAPIErrorDescription(with: data, response: response) + completion(.failure(OptableError.witness(errDesc, code: response.statusCode))) return } completion(.success(response)) - }?.resume() + })?.resume() } func _profile(traits: NSDictionary, completion: @escaping (Result) -> Void) throws { - try api.profile(traits: traits) { data, response, error in + try api.profile(traits: traits, completionHandler: { data, response, error in guard let response = response as? HTTPURLResponse, error == nil else { if let err = error { completion(.failure(OptableError.profile("Session error: \(err)"))) @@ -423,16 +408,21 @@ private extension OptableSDK { } return } - guard 200 ..< 300 ~= response.statusCode else { - var msg = "HTTP response.statusCode: \(response.statusCode)" - do { - let json = try JSONSerialization.jsonObject(with: data ?? Data(), options: []) - msg += ", data: \(json)" - } catch {} - completion(.failure(OptableError.profile(msg))) + guard case .successful = HTTPStatusCode(statusCode: response.statusCode) else { + let errDesc = OptableSDK.generateEdgeAPIErrorDescription(with: data, response: response) + completion(.failure(OptableError.profile(errDesc, code: response.statusCode))) return } completion(.success(response)) - }?.resume() + })?.resume() + } + + private static func generateEdgeAPIErrorDescription(with data: Data?, response: HTTPURLResponse) -> String { + var msg = "HTTP response.statusCode: \(response.statusCode)" + do { + let json = try JSONSerialization.jsonObject(with: data ?? Data(), options: []) + msg += ", data: \(json)" + } catch {} + return msg } } From 54f6830a8cce51ee35be47c75ba19125b2018059 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Wed, 17 Dec 2025 14:32:25 +0100 Subject: [PATCH 15/42] - Added `generateEnrichedIds` for OptableIdentifiers (on encode) --- Source/Core/OptableIdentifierEncoder.swift | 47 ++++++++++++---------- Source/Core/OptableIdentifiers.swift | 36 +++++++++++++++-- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/Source/Core/OptableIdentifierEncoder.swift b/Source/Core/OptableIdentifierEncoder.swift index ce39ce9..a4b3763 100644 --- a/Source/Core/OptableIdentifierEncoder.swift +++ b/Source/Core/OptableIdentifierEncoder.swift @@ -13,10 +13,9 @@ import Foundation #endif // MARK: - OptableIdentifierEncoder -@objc -final class OptableIdentifierEncoder: NSObject { +enum OptableIdentifierEncoder { /// Builds Enriched Identifier from Email address - func email(_ email: String) -> String { + static func email(_ email: String) -> String { let prefix = OptableIdentifier.emailAddress.rawValue let normalizedData = Data(email.lowercased().trimmingCharacters(in: .whitespacesAndNewlines).utf8) let identifier = sha256(data: normalizedData) @@ -24,7 +23,7 @@ final class OptableIdentifierEncoder: NSObject { } /// Builds Enriched Identifier from Phone number - func phoneNumber(_ phoneNumber: String) -> String { + static func phoneNumber(_ phoneNumber: String) -> String { let prefix = OptableIdentifier.phoneNumber.rawValue let normalizedData = Data(phoneNumber.lowercased().trimmingCharacters(in: .whitespacesAndNewlines).utf8) let identifier = sha256(data: normalizedData) @@ -32,89 +31,95 @@ final class OptableIdentifierEncoder: NSObject { } /// Builds Enriched Identifier from Postal code - func postalCode(_ postalCode: String) -> String { + static func postalCode(_ postalCode: String) -> String { let prefix = OptableIdentifier.postalCode.rawValue let identifier = postalCode.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from IPv4 address - func ipv4(_ ipv4: String) -> String { + static func ipv4(_ ipv4: String) -> String { let prefix = OptableIdentifier.ipv4Address.rawValue let identifier = ipv4.trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from IPv6 address - func ipv6(_ ipv6: String) -> String { + static func ipv6(_ ipv6: String) -> String { let prefix = OptableIdentifier.ipv6Address.rawValue let identifier = ipv6.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Apple IDFA - func idfa(_ idfa: String) -> String { + static func idfa(_ idfa: String) -> String { let prefix = OptableIdentifier.appleIDFA.rawValue let identifier = idfa.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Google GAID - func gaid(_ gaid: String) -> String { + static func gaid(_ gaid: String) -> String { let prefix = OptableIdentifier.googleGAID.rawValue let identifier = gaid.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Roku RIDA - func rida(_ rida: String) -> String { + static func rida(_ rida: String) -> String { let prefix = OptableIdentifier.rokuRIDA.rawValue let identifier = rida.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Samsung TV TIFA - func tifa(_ tifa: String) -> String { + static func tifa(_ tifa: String) -> String { let prefix = OptableIdentifier.samsungTIFA.rawValue let identifier = tifa.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Amazon Fire AFAI - func afai(_ afai: String) -> String { + static func afai(_ afai: String) -> String { let prefix = OptableIdentifier.amazonFireAFAI.rawValue let identifier = afai.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from NetID - func netid(_ netid: String) -> String { + static func netid(_ netid: String) -> String { let prefix = OptableIdentifier.netID.rawValue let identifier = netid.trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from ID5 - func id5(_ id5: String) -> String { + static func id5(_ id5: String) -> String { let prefix = OptableIdentifier.id5.rawValue let identifier = id5.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Utiq - func utiq(_ utiq: String) -> String { + static func utiq(_ utiq: String) -> String { let prefix = OptableIdentifier.utiq.rawValue let identifier = utiq.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Custom Publisher Provided ID (PPID) - @objc - func custom(_ idx: Int = 0, _ ppid: String) -> String { + static func custom(_ idx: Int = 0, _ ppid: String) -> String { let prefix = OptableIdentifier.custom(idx).rawValue let identifier = ppid.trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } + + /// Builds Enriched Identifier from Optable Visitor ID (VID) + static func vid(_ vid: String) -> String { + let prefix = OptableIdentifier.optableVID.rawValue + let identifier = vid.trimmingCharacters(in: .whitespacesAndNewlines) + return "\(prefix):\(identifier)" + } /// /// eidFromURL(urlString) is a helper that returns a type-prefixed ID based on @@ -126,7 +131,7 @@ final class OptableIdentifierEncoder: NSObject { /// encoded links in newsletter Emails sent by the application developer. Such /// hashed Email values can be used in calls to identify() /// - func eidFromURL(_ urlString: String) -> String { + static func eidFromURL(_ urlString: String) -> String { guard let url = URL(string: urlString) else { return "" } guard let urlc = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return "" } guard let urlqis = urlc.queryItems else { return "" } @@ -154,7 +159,7 @@ final class OptableIdentifierEncoder: NSObject { } // MARK: - Private - private func sha256(data: Data) -> String { + private static func sha256(data: Data) -> String { #if canImport(CryptoKit) if #available(iOS 13.0, *) { return SHA256 @@ -164,10 +169,10 @@ final class OptableIdentifierEncoder: NSObject { } #endif - return self.cchash(data) + return cchash(data) } - private func cchash(_ input: Data) -> String { + private static func cchash(_ input: Data) -> String { var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) input.withUnsafeBytes { bytes in _ = CC_SHA256(bytes.baseAddress, CC_LONG(input.count), &digest) diff --git a/Source/Core/OptableIdentifiers.swift b/Source/Core/OptableIdentifiers.swift index 7d8026a..c741074 100644 --- a/Source/Core/OptableIdentifiers.swift +++ b/Source/Core/OptableIdentifiers.swift @@ -31,17 +31,45 @@ struct OptableIdentifiers { get { dict[key] } set { dict[key] = newValue } } + + func generateEnrichedIds() -> [String] { + var results: [String] = [] + + for (key, value) in dict { + guard let optableIdentifier = OptableIdentifier(rawValue: key) else { continue } + + let eid: String = switch optableIdentifier { + case .emailAddress: OptableIdentifierEncoder.email(value) + case .phoneNumber: OptableIdentifierEncoder.phoneNumber(value) + case .postalCode: OptableIdentifierEncoder.postalCode(value) + case .ipv4Address: OptableIdentifierEncoder.ipv4(value) + case .ipv6Address: OptableIdentifierEncoder.ipv6(value) + case .appleIDFA: OptableIdentifierEncoder.idfa(value) + case .googleGAID: OptableIdentifierEncoder.gaid(value) + case .rokuRIDA: OptableIdentifierEncoder.rida(value) + case .samsungTIFA: OptableIdentifierEncoder.tifa(value) + case .amazonFireAFAI: OptableIdentifierEncoder.afai(value) + case .netID: OptableIdentifierEncoder.netid(value) + case .id5: OptableIdentifierEncoder.id5(value) + case .utiq: OptableIdentifierEncoder.utiq(value) + case let .custom(idx): OptableIdentifierEncoder.custom(idx ?? 0, value) + case .optableVID: OptableIdentifierEncoder.vid(value) + } + results.append(eid) + } + + return results + } } // MARK: - Encodable extension OptableIdentifiers: Encodable { func encode(to encoder: any Encoder) throws { - guard dict.isEmpty == false else { return } + let enrichedIds = generateEnrichedIds() var container = encoder.unkeyedContainer() - for (key, value) in dict { - let enrichedIdentifier = "\(key):\(value)" - try container.encode(enrichedIdentifier) + for eid in enrichedIds { + try container.encode(eid) } } } From 66b52152d94958477855865a944af0a0f1db396d Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Wed, 17 Dec 2025 16:04:39 +0100 Subject: [PATCH 16/42] - Added EdgeAPITests --- OptableSDK.xcodeproj/project.pbxproj | 2 + Tests/EdgeAPITests.swift | 57 ++++++++++++++++++++++++++++ Tests/Misc/cartesianProduct.swift | 22 +++++++++++ 3 files changed, 81 insertions(+) create mode 100644 Tests/EdgeAPITests.swift create mode 100644 Tests/Misc/cartesianProduct.swift diff --git a/OptableSDK.xcodeproj/project.pbxproj b/OptableSDK.xcodeproj/project.pbxproj index 45df223..f0b6fc5 100644 --- a/OptableSDK.xcodeproj/project.pbxproj +++ b/OptableSDK.xcodeproj/project.pbxproj @@ -39,6 +39,8 @@ CEBE038D2EF03ADD0027D67F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + EdgeAPITests.swift, + Misc/cartesianProduct.swift, OptableSDKTests.swift, ); target = 6352AB0324EAD403002E66EB /* OptableSDKTests */; diff --git a/Tests/EdgeAPITests.swift b/Tests/EdgeAPITests.swift new file mode 100644 index 0000000..6aeb715 --- /dev/null +++ b/Tests/EdgeAPITests.swift @@ -0,0 +1,57 @@ +// +// EdgeAPITests.swift +// OptableSDK +// +// Created by user on 17.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +@testable import OptableSDK +import XCTest + +class EdgeAPITests: XCTestCase { + typealias TestCaseConfiguration = (insecure: Bool, host: String, path: String, endpoint: String, tenant: String, slug: String) + var defaultTestConfiguration: TestCaseConfiguration { + (insecure: false, host: "na.edge.optable.co", path: "v2", endpoint: "", tenant: "test-tenant", slug: "test-slug") + } + + /** + Expected output: + `https://{{Domain}}/{{API_ENDPOINT}}?t={{TENANT}}&o={{SOURCE_SLUG}}` + + For more info check: + [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide) + */ + func test_edge_api_url_generation() throws { + + let hosts = ["na.edge.optable.co", "au.edge.optable.co", "jp.edge.optable.co", "eu.edge.optable.co"] + let endpoints = ["identify", "profile", "targeting", "witness", "tokenize"] + let paths = ["v2"] + let tenants = ["prebidtest", "test-tenant"] + let slugs = ["ios-sdk", "js-sdk"] + + cartesianProduct([hosts, paths, endpoints, tenants, slugs]) + .map({ product in + var testConfig = defaultTestConfiguration + testConfig.host = product[0] + testConfig.path = product[1] + testConfig.endpoint = product[2] + testConfig.tenant = product[3] + testConfig.slug = product[4] + return testConfig + }) + .forEach({ (testConfig: TestCaseConfiguration) in + let edgeAPI = EdgeAPI(OptableConfig(tenant: testConfig.tenant, originSlug: testConfig.slug, host: testConfig.host, path: testConfig.path, insecure: testConfig.insecure)) + let generatedURL = edgeAPI.buildEdgeAPIURL(endpoint: testConfig.endpoint) + let generatedURLComponents = URLComponents(url: generatedURL!, resolvingAgainstBaseURL: false)! + + XCTAssertEqual(generatedURLComponents.scheme, testConfig.insecure ? "http" : "https") + XCTAssertEqual(generatedURLComponents.host, testConfig.host) + XCTAssertEqual(generatedURLComponents.path, "/\(testConfig.path)/\(testConfig.endpoint)") + XCTAssertNotNil(generatedURLComponents.queryItems?.first(where: { $0.name == "t" })) + XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "t" })!.value, testConfig.tenant) + XCTAssertNotNil(generatedURLComponents.queryItems?.first(where: { $0.name == "o" })) + XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "o" })!.value, testConfig.slug) + }) + } +} diff --git a/Tests/Misc/cartesianProduct.swift b/Tests/Misc/cartesianProduct.swift new file mode 100644 index 0000000..c1049d7 --- /dev/null +++ b/Tests/Misc/cartesianProduct.swift @@ -0,0 +1,22 @@ +// +// XCTAssertEqual.swift +// OptableSDK +// +// Created by user on 17.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +import Foundation + +/// Returns the Cartesian product of multiple arrays +/// - Parameter arrays: An array of arrays of type T +/// - Returns: Array of arrays representing all combinations +func cartesianProduct(_ arrays: [[T]]) -> [[T]] { + arrays.reduce([[]] as [[T]]) { acc, array in + acc.flatMap { prefix in + array.map { element in + prefix + [element] + } + } + } +} From 323faf55cb77cbe4ed3a0f61d2faf943ae898cbd Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Wed, 17 Dec 2025 17:33:00 +0100 Subject: [PATCH 17/42] - Updated identifier encoding --- Source/Core/OptableIdentifierEncoder.swift | 32 +++++++++++----------- Source/Core/OptableIdentifiers.swift | 4 +-- Source/OptableIdentifier.swift | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Source/Core/OptableIdentifierEncoder.swift b/Source/Core/OptableIdentifierEncoder.swift index a4b3763..d6b0628 100644 --- a/Source/Core/OptableIdentifierEncoder.swift +++ b/Source/Core/OptableIdentifierEncoder.swift @@ -17,7 +17,7 @@ enum OptableIdentifierEncoder { /// Builds Enriched Identifier from Email address static func email(_ email: String) -> String { let prefix = OptableIdentifier.emailAddress.rawValue - let normalizedData = Data(email.lowercased().trimmingCharacters(in: .whitespacesAndNewlines).utf8) + let normalizedData = Data(email.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased().utf8) let identifier = sha256(data: normalizedData) return "\(prefix):\(identifier)" } @@ -25,7 +25,7 @@ enum OptableIdentifierEncoder { /// Builds Enriched Identifier from Phone number static func phoneNumber(_ phoneNumber: String) -> String { let prefix = OptableIdentifier.phoneNumber.rawValue - let normalizedData = Data(phoneNumber.lowercased().trimmingCharacters(in: .whitespacesAndNewlines).utf8) + let normalizedData = Data(phoneNumber.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased().utf8) let identifier = sha256(data: normalizedData) return "\(prefix):\(identifier)" } @@ -33,91 +33,91 @@ enum OptableIdentifierEncoder { /// Builds Enriched Identifier from Postal code static func postalCode(_ postalCode: String) -> String { let prefix = OptableIdentifier.postalCode.rawValue - let identifier = postalCode.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + let identifier = postalCode.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from IPv4 address static func ipv4(_ ipv4: String) -> String { let prefix = OptableIdentifier.ipv4Address.rawValue - let identifier = ipv4.trimmingCharacters(in: .whitespacesAndNewlines) + let identifier = ipv4.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from IPv6 address static func ipv6(_ ipv6: String) -> String { let prefix = OptableIdentifier.ipv6Address.rawValue - let identifier = ipv6.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + let identifier = ipv6.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Apple IDFA static func idfa(_ idfa: String) -> String { let prefix = OptableIdentifier.appleIDFA.rawValue - let identifier = idfa.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + let identifier = idfa.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Google GAID static func gaid(_ gaid: String) -> String { let prefix = OptableIdentifier.googleGAID.rawValue - let identifier = gaid.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + let identifier = gaid.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Roku RIDA static func rida(_ rida: String) -> String { let prefix = OptableIdentifier.rokuRIDA.rawValue - let identifier = rida.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + let identifier = rida.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Samsung TV TIFA static func tifa(_ tifa: String) -> String { let prefix = OptableIdentifier.samsungTIFA.rawValue - let identifier = tifa.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + let identifier = tifa.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Amazon Fire AFAI static func afai(_ afai: String) -> String { let prefix = OptableIdentifier.amazonFireAFAI.rawValue - let identifier = afai.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + let identifier = afai.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from NetID static func netid(_ netid: String) -> String { let prefix = OptableIdentifier.netID.rawValue - let identifier = netid.trimmingCharacters(in: .whitespacesAndNewlines) + let identifier = netid.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from ID5 static func id5(_ id5: String) -> String { let prefix = OptableIdentifier.id5.rawValue - let identifier = id5.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + let identifier = id5.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Utiq static func utiq(_ utiq: String) -> String { let prefix = OptableIdentifier.utiq.rawValue - let identifier = utiq.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + let identifier = utiq.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Custom Publisher Provided ID (PPID) - static func custom(_ idx: Int = 0, _ ppid: String) -> String { + static func custom(idx: Int = 0, _ ppid: String) -> String { let prefix = OptableIdentifier.custom(idx).rawValue let identifier = ppid.trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } - + /// Builds Enriched Identifier from Optable Visitor ID (VID) static func vid(_ vid: String) -> String { let prefix = OptableIdentifier.optableVID.rawValue - let identifier = vid.trimmingCharacters(in: .whitespacesAndNewlines) + let identifier = vid.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined() return "\(prefix):\(identifier)" } diff --git a/Source/Core/OptableIdentifiers.swift b/Source/Core/OptableIdentifiers.swift index c741074..0ddc7a7 100644 --- a/Source/Core/OptableIdentifiers.swift +++ b/Source/Core/OptableIdentifiers.swift @@ -14,7 +14,7 @@ struct OptableIdentifiers { init() {} - init(_ dict: [String: String]) { + init(_ dict: [String: String] = [:]) { self.dict = dict } @@ -52,7 +52,7 @@ struct OptableIdentifiers { case .netID: OptableIdentifierEncoder.netid(value) case .id5: OptableIdentifierEncoder.id5(value) case .utiq: OptableIdentifierEncoder.utiq(value) - case let .custom(idx): OptableIdentifierEncoder.custom(idx ?? 0, value) + case let .custom(idx): OptableIdentifierEncoder.custom(idx: idx ?? 0, value) case .optableVID: OptableIdentifierEncoder.vid(value) } results.append(eid) diff --git a/Source/OptableIdentifier.swift b/Source/OptableIdentifier.swift index a488f9a..f6b9745 100644 --- a/Source/OptableIdentifier.swift +++ b/Source/OptableIdentifier.swift @@ -86,7 +86,7 @@ public enum OptableIdentifier: RawRepresentable, Hashable { case .id5: return "id5" case .utiq: return "utiq" case .custom(nil): return "c" - case let .custom(n?): return "c\(n)" + case let .custom(n?): return abs(n) == 0 ? "c" : "c\(abs(n))" case .optableVID: return "v" } } From 6cc900dfe5e6df93d62c522921d5662540a73562 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Wed, 17 Dec 2025 17:36:29 +0100 Subject: [PATCH 18/42] - Added new UnitTests --- OptableSDK.xcodeproj/project.pbxproj | 2 + Tests/EdgeAPITests.swift | 6 +- Tests/OptableIdentifierEncoderTests.swift | 160 ++++++++++++++++++++++ Tests/OptableIdentifiersTests.swift | 57 ++++++++ Tests/OptableSDKTests.swift | 108 ++------------- 5 files changed, 230 insertions(+), 103 deletions(-) create mode 100644 Tests/OptableIdentifierEncoderTests.swift create mode 100644 Tests/OptableIdentifiersTests.swift diff --git a/OptableSDK.xcodeproj/project.pbxproj b/OptableSDK.xcodeproj/project.pbxproj index f0b6fc5..ce464d5 100644 --- a/OptableSDK.xcodeproj/project.pbxproj +++ b/OptableSDK.xcodeproj/project.pbxproj @@ -41,6 +41,8 @@ membershipExceptions = ( EdgeAPITests.swift, Misc/cartesianProduct.swift, + OptableIdentifierEncoderTests.swift, + OptableIdentifiersTests.swift, OptableSDKTests.swift, ); target = 6352AB0324EAD403002E66EB /* OptableSDKTests */; diff --git a/Tests/EdgeAPITests.swift b/Tests/EdgeAPITests.swift index 6aeb715..e18b602 100644 --- a/Tests/EdgeAPITests.swift +++ b/Tests/EdgeAPITests.swift @@ -24,9 +24,9 @@ class EdgeAPITests: XCTestCase { */ func test_edge_api_url_generation() throws { - let hosts = ["na.edge.optable.co", "au.edge.optable.co", "jp.edge.optable.co", "eu.edge.optable.co"] - let endpoints = ["identify", "profile", "targeting", "witness", "tokenize"] - let paths = ["v2"] + let hosts = ["na.edge.optable.co", "au.edge.optable.co"] + let endpoints = ["identify", "profile"] + let paths = ["v1", "v2"] let tenants = ["prebidtest", "test-tenant"] let slugs = ["ios-sdk", "js-sdk"] diff --git a/Tests/OptableIdentifierEncoderTests.swift b/Tests/OptableIdentifierEncoderTests.swift new file mode 100644 index 0000000..e3837b0 --- /dev/null +++ b/Tests/OptableIdentifierEncoderTests.swift @@ -0,0 +1,160 @@ +// +// OptableIdentifierEncoderTests.swift +// OptableSDK +// +// Created by user on 17.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +@testable import OptableSDK +import XCTest + +class OptableIdentifierEncoderTests: XCTestCase { + typealias SUT = OptableIdentifierEncoder + + func test_email() throws { + var expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" + XCTAssertEqual(expected, SUT.email("123")) + XCTAssertEqual(expected, SUT.email(" 123")) + XCTAssertEqual(expected, SUT.email("123 ")) + XCTAssertEqual(expected, SUT.email(" 123 ")) + + expected = "e:9e9bff5609b2e4b721e682ce7a0759d4f042819bc15a698bcb99db7897555239" + XCTAssertEqual(expected, SUT.email("tEsT@ FooBarBaz.CoM")) + XCTAssertEqual(expected, SUT.email(" test@foobarbaz.com")) + XCTAssertEqual(expected, SUT.email("TEST@FOOBARBAZ.COM ")) + XCTAssertEqual(expected, SUT.email("TeSt@ f O O b A R b A Z.cOm")) + } + + func test_phoneNumber() throws { + let expected = "p:ebad3b64ae96005048fca1af2f15e5251ad3844d00fb80252711de9b651c8e46" + XCTAssertEqual(expected, SUT.phoneNumber("+33 555 456789")) + XCTAssertEqual(expected, SUT.phoneNumber("+33555456789")) + XCTAssertEqual(expected, SUT.phoneNumber("+3 3 5 5 5456789")) + XCTAssertEqual(expected, SUT.phoneNumber(" +33555456789 ")) + } + + func test_postalCode() throws { + XCTAssertEqual("z:m5v3l9", SUT.postalCode(" M5V 3L9")) + XCTAssertEqual("z:t2p5h1", SUT.postalCode("T 2 P 5 H 1")) + XCTAssertEqual("z:90210", SUT.postalCode("90210")) + XCTAssertEqual("z:10001", SUT.postalCode("10001")) + XCTAssertEqual("z:sw1a1aa", SUT.postalCode("SW1A 1AA")) + XCTAssertEqual("z:eh11bb", SUT.postalCode("EH1 1BB")) + } + + func test_id5() throws { + let expected = "id5:ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg" + XCTAssertEqual(expected, SUT.id5(" ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg ")) + XCTAssertEqual(expected, SUT.id5("ID5*UDWnp 3JOtWV0ky-bHvE eU4xOVHXCmYeg2 4YigF8iAymU HplfYSEl M3fy79h8p-Fg")) + } + + func test_utiq() throws { + let expected = "utiq:496f5db5-681f-4392-acd5-0d4f6e2f6b88" + XCTAssertEqual(expected, SUT.utiq("496f5DB5-681F-4392-aCD5-0d4f6e2f6b88")) + XCTAssertEqual(expected, SUT.utiq(" 496f5db5 -681f -4392- acd5-0d4f6e2f6b88 ")) + } + + func test_ipv4() throws { + let expected = "i4:8.8.8.8" + XCTAssertEqual(expected, SUT.ipv4("8.8.8.8")) + XCTAssertEqual(expected, SUT.ipv4(" 8. 8. 8. 8 ")) + } + + func test_ipv6() throws { + let expected = "i6:2001:0db8:85a3:0000:0000:8a2e:0370:7334" + XCTAssertEqual(expected, SUT.ipv6("2001:0DB8:85A3:0000:0000:8a2e:0370:7334")) + XCTAssertEqual(expected, SUT.ipv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334")) + } + + func test_idfa() throws { + let expected = "a:496f5db5-681f-4392-acd5-0d4f6e2f6b88" + XCTAssertEqual(expected, SUT.idfa("496f5DB5-681F-4392-acd5-0d4f6e2f6b88")) + XCTAssertEqual(expected, SUT.idfa("496f5db5- 681f- 4392- acd5- 0d4f6e2f6b88")) + } + + func test_gaid() throws { + let expected = "g:64873d9f-d5af-4770-8bcb-167a220eb17d" + XCTAssertEqual(expected, SUT.gaid("64873d9f-d5AF-4770-8bcb-167a220eb17d")) + XCTAssertEqual(expected, SUT.gaid(" 64873d9f- d5af-4770- 8bcb-167a220eb17d ")) + } + + func test_rida() throws { + let expected = "r:0b179df0-6cd5-49f1-be21-425d002e0d22" + XCTAssertEqual(expected, SUT.rida("0b179df0-6CD5-49f1-be21-425d002e0d22")) + XCTAssertEqual(expected, SUT.rida(" 0b179df0 -6cd5- 49f1-be21-425d002e0d22 ")) + } + + func test_tifa() throws { + let expected = "s:e0ef86a8-6ebf-4c9d-9127-e69407fe748d" + XCTAssertEqual(expected, SUT.tifa("e0ef86a8-6EBf-4c9d-9127-e69407fe748d")) + XCTAssertEqual(expected, SUT.tifa(" e0ef86a8- 6ebf-4c9 d-9127-e69407fe748d ")) + } + + func test_afai() throws { + let expected = "f:6e853799-ef31-4a30-8706-9742be254d38" + XCTAssertEqual(expected, SUT.afai("6E853799-EF31-4a30-8706-9742be254d38")) + XCTAssertEqual(expected, SUT.afai(" 6 e853799- ef31-4a30-8706-9742be254d38 ")) + } + + func test_netid() throws { + let expected = "n:_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ" + XCTAssertEqual(expected, SUT.netid(" _YV2v2Uhx3vqe H47Rrhzgr-4c3VNs xis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ ")) + XCTAssertEqual(expected, SUT.netid("_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ")) + } + + func test_custom() throws { + let expected = "c:FooBarBAZ-01234#98765.!!!" + XCTAssertEqual(expected, SUT.custom("FooBarBAZ-01234#98765.!!!")) + XCTAssertEqual(expected, SUT.custom(" FooBarBAZ-01234#98765.!!!")) + XCTAssertEqual(expected, SUT.custom("FooBarBAZ-01234#98765.!!! ")) + XCTAssertEqual(expected, SUT.custom(" FooBarBAZ-01234#98765.!!! ")) + + // Case sensitive + let unexpected = "c:FooBarBAZ-01234#98765.!!!" + XCTAssertNotEqual(unexpected, SUT.custom("foobarBAZ-01234#98765.!!!")) + } + + // MARK: Legacy + func test_eidFromURL_isCorrect() throws { + let url = "http://some.domain.com/some/path?some=query&something=else&oeid=a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3&foo=bar&baz" + let expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" + + XCTAssertEqual(expected, SUT.eidFromURL(url)) + } + + func test_eidFromURL_returnsEmptyWhenArgEmpty() throws { + let url = "" + let expected = "" + + XCTAssertEqual(expected, SUT.eidFromURL(url)) + } + + func test_eidFromURL_returnsEmptyWhenOeidAbsentFromQuerystring() throws { + let url = "http://some.domain.com/some/path?some=query&something=else" + let expected = "" + + XCTAssertEqual(expected, SUT.eidFromURL(url)) + } + + func test_eidFromURL_returnsEmptyWhenQuerystringAbsent() throws { + let url = "http://some.domain.com/some/path" + let expected = "" + + XCTAssertEqual(expected, SUT.eidFromURL(url)) + } + + func test_eidFromURL_expectsSHA256() throws { + let url = "http://some.domain.com/some/path?some=query&something=else&oeid=AAAAAAAa665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3&foo=bar&baz" + let expected = "" + + XCTAssertEqual(expected, SUT.eidFromURL(url)) + } + + func test_eidFromURL_ignoresCase() throws { + let url = "http://some.domain.com/some/path?some=query&something=else&oEId=A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86f7f7A27AE3&foo=bar&baz" + let expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" + + XCTAssertEqual(expected, SUT.eidFromURL(url)) + } +} diff --git a/Tests/OptableIdentifiersTests.swift b/Tests/OptableIdentifiersTests.swift new file mode 100644 index 0000000..4faefeb --- /dev/null +++ b/Tests/OptableIdentifiersTests.swift @@ -0,0 +1,57 @@ +// +// OptableIdentifiersTests.swift +// OptableSDK +// +// Created by user on 17.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +@testable import OptableSDK +import XCTest + +class OptableIdentifiersTests: XCTestCase { + func test_json_generation_empty() throws { + let expected = "[]" + let oids = OptableIdentifiers() + let data = try JSONEncoder().encode(oids) + let generatedJSON = String(data: data, encoding: .utf8) + XCTAssertEqual(expected, generatedJSON) + } + + func test_json_generation_list() throws { + let oids = OptableIdentifiers([ + .emailAddress: "foo@bar.com", + .phoneNumber: "+15123465890", + .postalCode: "M5V 3L9", + .id5: "ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg", + .utiq: "496f5db5-681f-4392-acd5-0d4f6e2f6b88", + .ipv4Address: "8.8.8.8", + .ipv6Address: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + .appleIDFA: "496f5db5-681f-4392-acd5-0d4f6e2f6b88", + .googleGAID: "64873d9f-d5af-4770-8bcb-167a220eb17d", + .rokuRIDA: "0b179df0-6cd5-49f1-be21-425d002e0d22", + .samsungTIFA: "e0ef86a8-6ebf-4c9d-9127-e69407fe748d", + .amazonFireAFAI: "6e853799-ef31-4a30-8706-9742be254d38", + .netID: "_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ", + .custom(nil): "d29c551097b9dd0b82423827f65161232efaf7fc", + .custom(1): "AaaZza.dh012", + ]) + let encodedData = try JSONEncoder().encode(oids) + let decodedData = try JSONDecoder().decode([String].self, from: encodedData) + XCTAssertTrue(decodedData.contains(where: { $0 == "e:0c7e6a405862e402eb76a70f8a26fc732d07c32931e9fae9ab1582911d2e8a3b"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "p:f45562169005d99cdbb6908607fd5b50b66fd835a132a8225cc361d5692a8bd2"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "z:m5v3l9"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "id5:ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "utiq:496f5db5-681f-4392-acd5-0d4f6e2f6b88"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "i4:8.8.8.8"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "i6:2001:0db8:85a3:0000:0000:8a2e:0370:7334"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "a:496f5db5-681f-4392-acd5-0d4f6e2f6b88"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "g:64873d9f-d5af-4770-8bcb-167a220eb17d"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "r:0b179df0-6cd5-49f1-be21-425d002e0d22"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "s:e0ef86a8-6ebf-4c9d-9127-e69407fe748d"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "f:6e853799-ef31-4a30-8706-9742be254d38"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "n:_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "c:d29c551097b9dd0b82423827f65161232efaf7fc"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "c1:AaaZza.dh012"})) + } +} diff --git a/Tests/OptableSDKTests.swift b/Tests/OptableSDKTests.swift index f6d2677..d900931 100644 --- a/Tests/OptableSDKTests.swift +++ b/Tests/OptableSDKTests.swift @@ -6,111 +6,19 @@ // See LICENSE for details. // -import XCTest @testable import OptableSDK +import XCTest class OptableSDKTests: XCTestCase { - var sdk:OptableSDK! - - override func setUpWithError() throws { - sdk = OptableSDK.init(host: "127.0.0.1", app: "tests", insecure: true) - } - - override func tearDownWithError() throws { - } - - func test_eid_isCorrect() throws { - let expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" - XCTAssertEqual(expected, sdk.eid("123")) - XCTAssertEqual(expected, sdk.eid(" 123")) - XCTAssertEqual(expected, sdk.eid("123 ")) - XCTAssertEqual(expected, sdk.eid(" 123 ")) - } - - func test_eid_ignoresCase() throws { - let var1 = "tEsT@FooBarBaz.CoM" - let var2 = "test@foobarbaz.com" - let var3 = "TEST@FOOBARBAZ.COM" - let var4 = "TeSt@fOObARbAZ.cOm" - let eid = sdk.eid(var1) + var config: OptableConfig! + var sdk: OptableSDK! - XCTAssertEqual(eid, sdk.eid(var2)) - XCTAssertEqual(eid, sdk.eid(var3)) - XCTAssertEqual(eid, sdk.eid(var4)) - } - - func test_aaid_isCorrectAndIgnoresCase() throws { - let expected = "a:ea7583cd-a667-48bc-b806-42ecb2b48606" - - XCTAssertEqual(expected, sdk.aaid("ea7583cd-a667-48bc-b806-42ecb2b48606")) - XCTAssertEqual(expected, sdk.aaid(" ea7583cd-a667-48bc-b806-42ecb2b48606")) - XCTAssertEqual(expected, sdk.aaid("ea7583cd-a667-48bc-b806-42ecb2b48606 ")) - XCTAssertEqual(expected, sdk.aaid(" ea7583cd-a667-48bc-b806-42ecb2b48606 ")) - XCTAssertEqual(expected, sdk.aaid("EA7583CD-A667-48BC-B806-42ECB2B48606")) - } - - func test_cid_isCorrect() throws { - let expected = "c:FooBarBAZ-01234#98765.!!!" - - XCTAssertEqual(expected, sdk.cid("FooBarBAZ-01234#98765.!!!")) - XCTAssertEqual(expected, sdk.cid(" FooBarBAZ-01234#98765.!!!")) - XCTAssertEqual(expected, sdk.cid("FooBarBAZ-01234#98765.!!! ")) - XCTAssertEqual(expected, sdk.cid(" FooBarBAZ-01234#98765.!!! ")) - } - - func test_cid_isCaseSensitive() throws { - let unexpected = "c:FooBarBAZ-01234#98765.!!!" - - XCTAssertNotEqual(unexpected, sdk.cid("foobarBAZ-01234#98765.!!!")) - } + let defaultConfig = OptableConfig(tenant: "test-tenant", originSlug: "test-slug", insecure: false) - func test_eidFromURL_isCorrect() throws { - let url = "http://some.domain.com/some/path?some=query&something=else&oeid=a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3&foo=bar&baz" - let expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" - - XCTAssertEqual(expected, sdk.eidFromURL(url)) - } - - func test_eidFromURL_returnsEmptyWhenArgEmpty() throws { - let url = "" - let expected = "" - - XCTAssertEqual(expected, sdk.eidFromURL(url)) - } - - func test_eidFromURL_returnsEmptyWhenOeidAbsentFromQuerystring() throws { - let url = "http://some.domain.com/some/path?some=query&something=else" - let expected = "" - - XCTAssertEqual(expected, sdk.eidFromURL(url)) - } - - func test_eidFromURL_returnsEmptyWhenQuerystringAbsent() throws { - let url = "http://some.domain.com/some/path" - let expected = "" - - XCTAssertEqual(expected, sdk.eidFromURL(url)) - } - - func test_eidFromURL_expectsSHA256() throws { - let url = "http://some.domain.com/some/path?some=query&something=else&oeid=AAAAAAAa665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3&foo=bar&baz" - let expected = "" - - XCTAssertEqual(expected, sdk.eidFromURL(url)) - } - - func test_eidFromURL_ignoresCase() throws { - let url = "http://some.domain.com/some/path?some=query&something=else&oEId=A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86f7f7A27AE3&foo=bar&baz" - let expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" - - XCTAssertEqual(expected, sdk.eidFromURL(url)) - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } + override func setUpWithError() throws { + config = defaultConfig + sdk = OptableSDK(config: config) } + override func tearDownWithError() throws {} } From 00f02d7e7d37fe88f2ac1ee1d97015fc8ab1d30b Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Wed, 17 Dec 2025 19:54:35 +0100 Subject: [PATCH 19/42] - Added TODOs for integration tests --- Tests/OptableSDKTests.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/OptableSDKTests.swift b/Tests/OptableSDKTests.swift index d900931..711f0b7 100644 --- a/Tests/OptableSDKTests.swift +++ b/Tests/OptableSDKTests.swift @@ -21,4 +21,20 @@ class OptableSDKTests: XCTestCase { } override func tearDownWithError() throws {} + + func test_identify() throws { + // TODO: impl + } + + func test_target() throws { + // TODO: impl + } + + func test_witness() throws { + // TODO: impl + } + + func test_profile() throws { + // TODO: impl + } } From 12f82115373b8a4f8d42abface91cba5082ec439 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Wed, 17 Dec 2025 21:16:22 +0100 Subject: [PATCH 20/42] - Minor fixes --- Source/Core/AppTrackingTransparency.swift | 2 +- Source/Core/Networking.swift | 145 ++++++++-------------- Source/OptableSDK.swift | 12 +- 3 files changed, 57 insertions(+), 102 deletions(-) diff --git a/Source/Core/AppTrackingTransparency.swift b/Source/Core/AppTrackingTransparency.swift index a536db4..c49f5d8 100644 --- a/Source/Core/AppTrackingTransparency.swift +++ b/Source/Core/AppTrackingTransparency.swift @@ -26,7 +26,7 @@ ASIdentifierManager.shared().isAdvertisingTrackingEnabled } - static var adfaAvailable: Bool { + static var advertisingIdentifierAvailable: Bool { #if canImport(AppTrackingTransparency) if #available(iOS 14, *) { return trackingStatus == .authorized diff --git a/Source/Core/Networking.swift b/Source/Core/Networking.swift index d66e5f1..a1c598e 100644 --- a/Source/Core/Networking.swift +++ b/Source/Core/Networking.swift @@ -112,112 +112,67 @@ enum HTTPBody { } // MARK: - HTTPStatusCode -enum HTTPStatusCode { +enum HTTPStatusCode: Int { // 1xx Informational - case `continue` // 100 - case switchingProtocols // 101 - case processing // 102 + case `continue` = 100 + case switchingProtocols = 101 + case processing = 102 // 2xx Success - case ok // 200 - case created // 201 - case accepted // 202 - case nonAuthoritative // 203 - case noContent // 204 - case resetContent // 205 - case partialContent // 206 + case ok = 200 + case created = 201 + case accepted = 202 + case nonAuthoritative = 203 + case noContent = 204 + case resetContent = 205 + case partialContent = 206 // 3xx Redirection - case multipleChoices // 300 - case movedPermanently // 301 - case found // 302 - case seeOther // 303 - case notModified // 304 - case temporaryRedirect // 307 - case permanentRedirect // 308 + case multipleChoices = 300 + case movedPermanently = 301 + case found = 302 + case seeOther = 303 + case notModified = 304 + case temporaryRedirect = 307 + case permanentRedirect = 308 // 4xx Client Error - case badRequest // 400 - case unauthorized // 401 - case paymentRequired // 402 - case forbidden // 403 - case notFound // 404 - case methodNotAllowed // 405 - case notAcceptable // 406 - case conflict // 409 - case gone // 410 - case unsupportedMediaType // 415 - case tooManyRequests // 429 + case badRequest = 400 + case unauthorized = 401 + case paymentRequired = 402 + case forbidden = 403 + case notFound = 404 + case methodNotAllowed = 405 + case notAcceptable = 406 + case conflict = 409 + case gone = 410 + case unsupportedMediaType = 415 + case tooManyRequests = 429 // 5xx Server Error - case internalServerError // 500 - case notImplemented // 501 - case badGateway // 502 - case serviceUnavailable // 503 - case gatewayTimeout // 504 - - // Categories - case informational // 100 ..< 200 - case successful // 200 ..< 300 - case redirect // 300 ..< 400 - case clientError // 400 ..< 500 - case serverError // 500 ..< 600 - - // swiftlint:disable:next cyclomatic_complexity - init(statusCode: Int) { - self = switch statusCode { - // 1xx - case 100: .continue - case 101: .switchingProtocols - case 102: .processing - // 2xx - case 200: .ok - case 201: .created - case 202: .accepted - case 203: .nonAuthoritative - case 204: .noContent - case 205: .resetContent - case 206: .partialContent - // 3xx - case 300: .multipleChoices - case 301: .movedPermanently - case 302: .found - case 303: .seeOther - case 304: .notModified - case 307: .temporaryRedirect - case 308: .permanentRedirect - // 4xx - case 400: .badRequest - case 401: .unauthorized - case 402: .paymentRequired - case 403: .forbidden - case 404: .notFound - case 405: .methodNotAllowed - case 406: .notAcceptable - case 409: .conflict - case 410: .gone - case 415: .unsupportedMediaType - case 429: .tooManyRequests - // 5xx - case 500: .internalServerError - case 501: .notImplemented - case 502: .badGateway - case 503: .serviceUnavailable - case 504: .gatewayTimeout - // Ranges - case 100 ..< 200: .informational - case 200 ..< 300: .successful - case 300 ..< 400: .redirect - case 400 ..< 500: .clientError - case 500 ..< 600: .serverError - default: - .serverError - } + case internalServerError = 500 + case notImplemented = 501 + case badGateway = 502 + case serviceUnavailable = 503 + case gatewayTimeout = 504 + + var isInformational: Bool { + (100 ..< 200).contains(rawValue) } var isSuccess: Bool { - if case .successful = self { return true } - if case .ok = self { return true } - return false + (200 ..< 300).contains(rawValue) + } + + var isRedirect: Bool { + (300 ..< 400).contains(rawValue) + } + + var isClientError: Bool { + (400 ..< 500).contains(rawValue) + } + + var isServerError: Bool { + (500 ..< 600).contains(rawValue) } } diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index 386a2c9..6456886 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -326,7 +326,7 @@ private extension OptableSDK { var ids = ids if config.skipAdvertisingIdDetection == false, - ATT.adfaAvailable, + 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[.appleIDFA] = ATT.advertisingIdentifier.uuidString } @@ -340,7 +340,7 @@ private extension OptableSDK { } return } - guard case .successful = HTTPStatusCode(statusCode: response.statusCode) else { + guard HTTPStatusCode(rawValue: response.statusCode)?.isSuccess == true else { let errDesc = OptableSDK.generateEdgeAPIErrorDescription(with: data, response: response) completion(.failure(OptableError.identify(errDesc, code: response.statusCode))) return @@ -359,7 +359,7 @@ private extension OptableSDK { } return } - guard case .successful = HTTPStatusCode(statusCode: response.statusCode) else { + guard HTTPStatusCode(rawValue: response.statusCode)?.isSuccess == true else { let errDesc = OptableSDK.generateEdgeAPIErrorDescription(with: data, response: response) completion(.failure(OptableError.targeting(errDesc, code: response.statusCode))) return @@ -389,7 +389,7 @@ private extension OptableSDK { } return } - guard case .successful = HTTPStatusCode(statusCode: response.statusCode) else { + guard HTTPStatusCode(rawValue: response.statusCode)?.isSuccess == true else { let errDesc = OptableSDK.generateEdgeAPIErrorDescription(with: data, response: response) completion(.failure(OptableError.witness(errDesc, code: response.statusCode))) return @@ -408,7 +408,7 @@ private extension OptableSDK { } return } - guard case .successful = HTTPStatusCode(statusCode: response.statusCode) else { + guard HTTPStatusCode(rawValue: response.statusCode)?.isSuccess == true else { let errDesc = OptableSDK.generateEdgeAPIErrorDescription(with: data, response: response) completion(.failure(OptableError.profile(errDesc, code: response.statusCode))) return @@ -416,7 +416,7 @@ private extension OptableSDK { completion(.success(response)) })?.resume() } - + private static func generateEdgeAPIErrorDescription(with data: Data?, response: HTTPURLResponse) -> String { var msg = "HTTP response.statusCode: \(response.statusCode)" do { From acea2fe1dab592e893abf41ea9a0982e7939241c Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Thu, 18 Dec 2025 13:02:36 +0100 Subject: [PATCH 21/42] - Exposed OptableIdentifiers to public --- Source/Core/OptableIdentifierEncoder.swift | 30 ++--- Source/Core/OptableIdentifiers.swift | 75 ------------ ...fier.swift => OptableIdentifierType.swift} | 6 +- Source/OptableIdentifiers.swift | 112 ++++++++++++++++++ Source/OptableSDK.swift | 45 +------ Tests/OptableIdentifiersTests.swift | 84 ++++++++++--- 6 files changed, 201 insertions(+), 151 deletions(-) delete mode 100644 Source/Core/OptableIdentifiers.swift rename Source/{OptableIdentifier.swift => OptableIdentifierType.swift} (96%) create mode 100644 Source/OptableIdentifiers.swift diff --git a/Source/Core/OptableIdentifierEncoder.swift b/Source/Core/OptableIdentifierEncoder.swift index d6b0628..fef5d77 100644 --- a/Source/Core/OptableIdentifierEncoder.swift +++ b/Source/Core/OptableIdentifierEncoder.swift @@ -16,7 +16,7 @@ import Foundation enum OptableIdentifierEncoder { /// Builds Enriched Identifier from Email address static func email(_ email: String) -> String { - let prefix = OptableIdentifier.emailAddress.rawValue + let prefix = OptableIdentifierType.emailAddress.rawValue let normalizedData = Data(email.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased().utf8) let identifier = sha256(data: normalizedData) return "\(prefix):\(identifier)" @@ -24,7 +24,7 @@ enum OptableIdentifierEncoder { /// Builds Enriched Identifier from Phone number static func phoneNumber(_ phoneNumber: String) -> String { - let prefix = OptableIdentifier.phoneNumber.rawValue + let prefix = OptableIdentifierType.phoneNumber.rawValue let normalizedData = Data(phoneNumber.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased().utf8) let identifier = sha256(data: normalizedData) return "\(prefix):\(identifier)" @@ -32,91 +32,91 @@ enum OptableIdentifierEncoder { /// Builds Enriched Identifier from Postal code static func postalCode(_ postalCode: String) -> String { - let prefix = OptableIdentifier.postalCode.rawValue + let prefix = OptableIdentifierType.postalCode.rawValue let identifier = postalCode.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from IPv4 address static func ipv4(_ ipv4: String) -> String { - let prefix = OptableIdentifier.ipv4Address.rawValue + let prefix = OptableIdentifierType.ipv4Address.rawValue let identifier = ipv4.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from IPv6 address static func ipv6(_ ipv6: String) -> String { - let prefix = OptableIdentifier.ipv6Address.rawValue + let prefix = OptableIdentifierType.ipv6Address.rawValue let identifier = ipv6.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Apple IDFA static func idfa(_ idfa: String) -> String { - let prefix = OptableIdentifier.appleIDFA.rawValue + let prefix = OptableIdentifierType.appleIDFA.rawValue let identifier = idfa.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Google GAID static func gaid(_ gaid: String) -> String { - let prefix = OptableIdentifier.googleGAID.rawValue + let prefix = OptableIdentifierType.googleGAID.rawValue let identifier = gaid.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Roku RIDA static func rida(_ rida: String) -> String { - let prefix = OptableIdentifier.rokuRIDA.rawValue + let prefix = OptableIdentifierType.rokuRIDA.rawValue let identifier = rida.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Samsung TV TIFA static func tifa(_ tifa: String) -> String { - let prefix = OptableIdentifier.samsungTIFA.rawValue + let prefix = OptableIdentifierType.samsungTIFA.rawValue let identifier = tifa.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Amazon Fire AFAI static func afai(_ afai: String) -> String { - let prefix = OptableIdentifier.amazonFireAFAI.rawValue + let prefix = OptableIdentifierType.amazonFireAFAI.rawValue let identifier = afai.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from NetID static func netid(_ netid: String) -> String { - let prefix = OptableIdentifier.netID.rawValue + let prefix = OptableIdentifierType.netID.rawValue let identifier = netid.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from ID5 static func id5(_ id5: String) -> String { - let prefix = OptableIdentifier.id5.rawValue + let prefix = OptableIdentifierType.id5.rawValue let identifier = id5.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Utiq static func utiq(_ utiq: String) -> String { - let prefix = OptableIdentifier.utiq.rawValue + let prefix = OptableIdentifierType.utiq.rawValue let identifier = utiq.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased() return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Custom Publisher Provided ID (PPID) static func custom(idx: Int = 0, _ ppid: String) -> String { - let prefix = OptableIdentifier.custom(idx).rawValue + let prefix = OptableIdentifierType.custom(idx).rawValue let identifier = ppid.trimmingCharacters(in: .whitespacesAndNewlines) return "\(prefix):\(identifier)" } /// Builds Enriched Identifier from Optable Visitor ID (VID) static func vid(_ vid: String) -> String { - let prefix = OptableIdentifier.optableVID.rawValue + let prefix = OptableIdentifierType.optableVID.rawValue let identifier = vid.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined() return "\(prefix):\(identifier)" } diff --git a/Source/Core/OptableIdentifiers.swift b/Source/Core/OptableIdentifiers.swift deleted file mode 100644 index 0ddc7a7..0000000 --- a/Source/Core/OptableIdentifiers.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// OptableIdentifiers.swift -// OptableSDK -// -// Created by user on 15.12.2025. -// Copyright © 2025 Optable Technologies, Inc. All rights reserved. -// - -import Foundation - -// MARK: - OptableIdentifiers -struct OptableIdentifiers { - var dict: [String: String] = [:] - - init() {} - - init(_ dict: [String: String] = [:]) { - self.dict = dict - } - - init(_ dict: [OptableIdentifier: String]) { - self.dict = Dictionary(uniqueKeysWithValues: dict.map({ ($0.key.rawValue, $0.value) })) - } - - subscript(_ key: OptableIdentifier) -> String? { - get { dict[key.rawValue] } - set { dict[key.rawValue] = newValue } - } - - subscript(_ key: String) -> String? { - get { dict[key] } - set { dict[key] = newValue } - } - - func generateEnrichedIds() -> [String] { - var results: [String] = [] - - for (key, value) in dict { - guard let optableIdentifier = OptableIdentifier(rawValue: key) else { continue } - - let eid: String = switch optableIdentifier { - case .emailAddress: OptableIdentifierEncoder.email(value) - case .phoneNumber: OptableIdentifierEncoder.phoneNumber(value) - case .postalCode: OptableIdentifierEncoder.postalCode(value) - case .ipv4Address: OptableIdentifierEncoder.ipv4(value) - case .ipv6Address: OptableIdentifierEncoder.ipv6(value) - case .appleIDFA: OptableIdentifierEncoder.idfa(value) - case .googleGAID: OptableIdentifierEncoder.gaid(value) - case .rokuRIDA: OptableIdentifierEncoder.rida(value) - case .samsungTIFA: OptableIdentifierEncoder.tifa(value) - case .amazonFireAFAI: OptableIdentifierEncoder.afai(value) - case .netID: OptableIdentifierEncoder.netid(value) - case .id5: OptableIdentifierEncoder.id5(value) - case .utiq: OptableIdentifierEncoder.utiq(value) - case let .custom(idx): OptableIdentifierEncoder.custom(idx: idx ?? 0, value) - case .optableVID: OptableIdentifierEncoder.vid(value) - } - results.append(eid) - } - - return results - } -} - -// MARK: - Encodable -extension OptableIdentifiers: Encodable { - func encode(to encoder: any Encoder) throws { - let enrichedIds = generateEnrichedIds() - - var container = encoder.unkeyedContainer() - for eid in enrichedIds { - try container.encode(eid) - } - } -} diff --git a/Source/OptableIdentifier.swift b/Source/OptableIdentifierType.swift similarity index 96% rename from Source/OptableIdentifier.swift rename to Source/OptableIdentifierType.swift index f6b9745..1e97187 100644 --- a/Source/OptableIdentifier.swift +++ b/Source/OptableIdentifierType.swift @@ -1,5 +1,5 @@ // -// OptableIdentifier.swift +// OptableIdentifierType.swift // OptableSDK // // Created by user on 16.12.2025. @@ -13,9 +13,9 @@ import Foundation For more info check: [](https://docs.optable.co/optable-documentation/getting-started/reference/identifier-types) - + */ -public enum OptableIdentifier: RawRepresentable, Hashable { +public enum OptableIdentifierType: RawRepresentable, Hashable { // Personal identifiers case emailAddress // e case phoneNumber // p diff --git a/Source/OptableIdentifiers.swift b/Source/OptableIdentifiers.swift new file mode 100644 index 0000000..639f1ed --- /dev/null +++ b/Source/OptableIdentifiers.swift @@ -0,0 +1,112 @@ +// +// OptableIdentifiers.swift +// OptableSDK +// +// Created by user on 15.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +import Foundation + +// MARK: - OptableIdentifiers +/** + Optable Identifiers container + + For more info check: + [](https://docs.optable.co/optable-documentation/getting-started/reference/identifier-types) + + */ +public struct OptableIdentifiers { + public var dict: [String: String] = [:] + + public init( + emailAddress: String? = nil, + phoneNumber: String? = nil, + postalCode: String? = nil, + ipv4Address: String? = nil, + ipv6Address: String? = nil, + appleIDFA: String? = nil, + googleGAID: String? = nil, + rokuRIDA: String? = nil, + samsungTIFA: String? = nil, + amazonFireAFAI: String? = nil, + netID: String? = nil, + id5: String? = nil, + utiq: String? = nil, + custom: [String: String]? = nil + ) { + self.dict[OptableIdentifierType.emailAddress.rawValue] = emailAddress + self.dict[OptableIdentifierType.phoneNumber.rawValue] = phoneNumber + self.dict[OptableIdentifierType.postalCode.rawValue] = postalCode + self.dict[OptableIdentifierType.ipv4Address.rawValue] = ipv4Address + self.dict[OptableIdentifierType.ipv6Address.rawValue] = ipv6Address + self.dict[OptableIdentifierType.appleIDFA.rawValue] = appleIDFA + self.dict[OptableIdentifierType.googleGAID.rawValue] = googleGAID + self.dict[OptableIdentifierType.rokuRIDA.rawValue] = rokuRIDA + self.dict[OptableIdentifierType.samsungTIFA.rawValue] = samsungTIFA + self.dict[OptableIdentifierType.amazonFireAFAI.rawValue] = amazonFireAFAI + self.dict[OptableIdentifierType.netID.rawValue] = netID + self.dict[OptableIdentifierType.id5.rawValue] = id5 + self.dict[OptableIdentifierType.utiq.rawValue] = utiq + self.dict.merge(custom ?? [:], uniquingKeysWith: { _, new in new }) + } + + public init(_ dict: [String: String] = [:]) { + self.dict = dict + } + + public subscript(_ key: String) -> String? { + get { dict[key] } + set { dict[key] = newValue } + } + + init(_ dict: [OptableIdentifierType: String]) { + self.dict = Dictionary(uniqueKeysWithValues: dict.map({ ($0.key.rawValue, $0.value) })) + } + + subscript(_ key: OptableIdentifierType) -> String? { + get { dict[key.rawValue] } + set { dict[key.rawValue] = newValue } + } + + func generateEnrichedIds() -> [String] { + var results: [String] = [] + + for (key, value) in dict { + guard let optableIdentifier = OptableIdentifierType(rawValue: key) else { continue } + + let eid: String = switch optableIdentifier { + case .emailAddress: OptableIdentifierEncoder.email(value) + case .phoneNumber: OptableIdentifierEncoder.phoneNumber(value) + case .postalCode: OptableIdentifierEncoder.postalCode(value) + case .ipv4Address: OptableIdentifierEncoder.ipv4(value) + case .ipv6Address: OptableIdentifierEncoder.ipv6(value) + case .appleIDFA: OptableIdentifierEncoder.idfa(value) + case .googleGAID: OptableIdentifierEncoder.gaid(value) + case .rokuRIDA: OptableIdentifierEncoder.rida(value) + case .samsungTIFA: OptableIdentifierEncoder.tifa(value) + case .amazonFireAFAI: OptableIdentifierEncoder.afai(value) + case .netID: OptableIdentifierEncoder.netid(value) + case .id5: OptableIdentifierEncoder.id5(value) + case .utiq: OptableIdentifierEncoder.utiq(value) + case let .custom(idx): OptableIdentifierEncoder.custom(idx: idx ?? 0, value) + case .optableVID: OptableIdentifierEncoder.vid(value) + } + results.append(eid) + } + + return results + } +} + +// MARK: - Encodable +extension OptableIdentifiers: Encodable { + public func encode(to encoder: any Encoder) throws { + let enrichedIds = generateEnrichedIds() + + var container = encoder.unkeyedContainer() + for eid in enrichedIds { + try container.encode(eid) + } + } +} diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index 6456886..0b9f78d 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -80,30 +80,11 @@ public extension OptableSDK { it either the HTTPURLResponse on success, or an NSError on failure. ```swift // Example - optableSDK.identify([ - "e": "example@example.com", - "p": "1234567890", - ], completion) + optableSDK.identify(.init(emailAddress: "example@example.com", phoneNumber: "1234567890"), completion) ``` */ - func identify(_ ids: [String: String], _ completion: @escaping (Result) -> Void) throws { - try _identify(OptableIdentifiers(ids), completion: completion) - } - - /** - This is the swift-friendly version of the `identify(ids, completion)` API. - - You can use predefined `OptableIdentifier` enum easily provide keys. - ```swift - // Example - optableSDK.identify([ - .emailAddress: "example@example.com", - .phoneNumber: "1234567890", - ], completion) - ``` - */ - func identify(_ ids: [OptableIdentifier: String], _ completion: @escaping (Result) -> Void) throws { - try _identify(OptableIdentifiers(ids), completion: completion) + func identify(_ ids: OptableIdentifiers, _ completion: @escaping (Result) -> Void) throws { + try _identify(ids, completion: completion) } // MARK: Async/Await support @@ -113,26 +94,10 @@ public extension OptableSDK { Instead of completion callbacks, function have to be awaited. */ @available(iOS 13.0, *) - func identify(_ ids: [String: String]) async throws -> HTTPURLResponse { - return try await withCheckedThrowingContinuation({ [unowned self] continuation in - do { - try self._identify(OptableIdentifiers(ids), completion: { continuation.resume(with: $0) }) - } catch { - continuation.resume(throwing: error) - } - }) - } - - /** - This is the Swift Concurrency compatible version of the `identify(ids, completion)` API. - - Instead of completion callbacks, function have to be awaited. - */ - @available(iOS 13.0, *) - func identify(_ ids: [OptableIdentifier: String]) async throws -> HTTPURLResponse { + func identify(_ ids: OptableIdentifiers) async throws -> HTTPURLResponse { return try await withCheckedThrowingContinuation({ [unowned self] continuation in do { - try self._identify(OptableIdentifiers(ids), completion: { continuation.resume(with: $0) }) + try self._identify(ids, completion: { continuation.resume(with: $0) }) } catch { continuation.resume(throwing: error) } diff --git a/Tests/OptableIdentifiersTests.swift b/Tests/OptableIdentifiersTests.swift index 4faefeb..f359825 100644 --- a/Tests/OptableIdentifiersTests.swift +++ b/Tests/OptableIdentifiersTests.swift @@ -18,13 +18,55 @@ class OptableIdentifiersTests: XCTestCase { XCTAssertEqual(expected, generatedJSON) } - func test_json_generation_list() throws { + func test_json_generation_list_obj() throws { + let oids = OptableIdentifiers( + emailAddress: "foo@bar.com", + phoneNumber: "+15123465890", + postalCode: "M5V 3L9", + ipv4Address: "8.8.8.8", + ipv6Address: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + appleIDFA: "496f5db5-681f-4392-acd5-0d4f6e2f6b88", + googleGAID: "64873d9f-d5af-4770-8bcb-167a220eb17d", + 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", + custom: [ + "c": "d29c551097b9dd0b82423827f65161232efaf7fc", + "c1": "AaaZza.dh012", + ] + ) + try test_json_generation_list(oids: oids) + } + + func test_json_generation_list_raw_dict() throws { + let oids = OptableIdentifiers([ + "e": "foo@bar.com", + "p": "+15123465890", + "z": "M5V 3L9", + "i4": "8.8.8.8", + "i6": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "a": "496f5db5-681f-4392-acd5-0d4f6e2f6b88", + "g": "64873d9f-d5af-4770-8bcb-167a220eb17d", + "r": "0b179df0-6cd5-49f1-be21-425d002e0d22", + "s": "e0ef86a8-6ebf-4c9d-9127-e69407fe748d", + "f": "6e853799-ef31-4a30-8706-9742be254d38", + "n": "_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ", + "id5": "ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg", + "utiq": "496f5db5-681f-4392-acd5-0d4f6e2f6b88", + "c": "d29c551097b9dd0b82423827f65161232efaf7fc", + "c1": "AaaZza.dh012", + ]) + try test_json_generation_list(oids: oids) + } + + func test_json_generation_list_enum_dict() throws { let oids = OptableIdentifiers([ .emailAddress: "foo@bar.com", .phoneNumber: "+15123465890", .postalCode: "M5V 3L9", - .id5: "ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg", - .utiq: "496f5db5-681f-4392-acd5-0d4f6e2f6b88", .ipv4Address: "8.8.8.8", .ipv6Address: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", .appleIDFA: "496f5db5-681f-4392-acd5-0d4f6e2f6b88", @@ -33,25 +75,31 @@ class OptableIdentifiersTests: XCTestCase { .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", .custom(nil): "d29c551097b9dd0b82423827f65161232efaf7fc", .custom(1): "AaaZza.dh012", ]) + try test_json_generation_list(oids: oids) + } + + private func test_json_generation_list(oids: OptableIdentifiers) throws { let encodedData = try JSONEncoder().encode(oids) let decodedData = try JSONDecoder().decode([String].self, from: encodedData) - XCTAssertTrue(decodedData.contains(where: { $0 == "e:0c7e6a405862e402eb76a70f8a26fc732d07c32931e9fae9ab1582911d2e8a3b"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "p:f45562169005d99cdbb6908607fd5b50b66fd835a132a8225cc361d5692a8bd2"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "z:m5v3l9"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "id5:ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "utiq:496f5db5-681f-4392-acd5-0d4f6e2f6b88"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "i4:8.8.8.8"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "i6:2001:0db8:85a3:0000:0000:8a2e:0370:7334"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "a:496f5db5-681f-4392-acd5-0d4f6e2f6b88"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "g:64873d9f-d5af-4770-8bcb-167a220eb17d"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "r:0b179df0-6cd5-49f1-be21-425d002e0d22"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "s:e0ef86a8-6ebf-4c9d-9127-e69407fe748d"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "f:6e853799-ef31-4a30-8706-9742be254d38"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "n:_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "c:d29c551097b9dd0b82423827f65161232efaf7fc"})) - XCTAssertTrue(decodedData.contains(where: { $0 == "c1:AaaZza.dh012"})) + XCTAssertTrue(decodedData.contains(where: { $0 == "e:0c7e6a405862e402eb76a70f8a26fc732d07c32931e9fae9ab1582911d2e8a3b" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "p:f45562169005d99cdbb6908607fd5b50b66fd835a132a8225cc361d5692a8bd2" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "z:m5v3l9" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "id5:ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "utiq:496f5db5-681f-4392-acd5-0d4f6e2f6b88" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "i4:8.8.8.8" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "i6:2001:0db8:85a3:0000:0000:8a2e:0370:7334" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "a:496f5db5-681f-4392-acd5-0d4f6e2f6b88" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "g:64873d9f-d5af-4770-8bcb-167a220eb17d" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "r:0b179df0-6cd5-49f1-be21-425d002e0d22" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "s:e0ef86a8-6ebf-4c9d-9127-e69407fe748d" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "f:6e853799-ef31-4a30-8706-9742be254d38" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "n:_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "c:d29c551097b9dd0b82423827f65161232efaf7fc" })) + XCTAssertTrue(decodedData.contains(where: { $0 == "c1:AaaZza.dh012" })) } } From 1223619e3da1e095c947f5467ea67b5a83405a53 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Thu, 18 Dec 2025 13:03:07 +0100 Subject: [PATCH 22/42] - Updated demo-ios-swift --- demo-ios-swift/Podfile | 11 +- demo-ios-swift/Podfile.lock | 10 +- .../demo-ios-swift.xcodeproj/project.pbxproj | 338 ------------------ .../xcschemes/demo-ios-swift.xcscheme | 107 ++++++ .../demo-ios-swift/AppDelegate.swift | 15 +- .../IdentifyViewController.swift | 47 ++- demo-ios-swift/demo-ios-swiftTests/Info.plist | 22 -- .../demo_ios_swiftTests.swift | 34 -- .../demo-ios-swiftUITests/Info.plist | 22 -- .../demo_ios_swiftUITests.swift | 43 --- 10 files changed, 151 insertions(+), 498 deletions(-) create mode 100644 demo-ios-swift/demo-ios-swift.xcodeproj/xcshareddata/xcschemes/demo-ios-swift.xcscheme delete mode 100644 demo-ios-swift/demo-ios-swiftTests/Info.plist delete mode 100644 demo-ios-swift/demo-ios-swiftTests/demo_ios_swiftTests.swift delete mode 100644 demo-ios-swift/demo-ios-swiftUITests/Info.plist delete mode 100644 demo-ios-swift/demo-ios-swiftUITests/demo_ios_swiftUITests.swift diff --git a/demo-ios-swift/Podfile b/demo-ios-swift/Podfile index 15aa921..82b47c6 100644 --- a/demo-ios-swift/Podfile +++ b/demo-ios-swift/Podfile @@ -1,4 +1,4 @@ -platform :ios, '14.0' +platform :ios, '15.0' source 'https://cdn.cocoapods.org/' @@ -13,13 +13,4 @@ target 'demo-ios-swift' do pod 'Google-Mobile-Ads-SDK' - target 'demo-ios-swiftTests' do - inherit! :search_paths - # Pods for testing - end - - target 'demo-ios-swiftUITests' do - # Pods for testing - end - end diff --git a/demo-ios-swift/Podfile.lock b/demo-ios-swift/Podfile.lock index e77c5d6..fa3644b 100644 --- a/demo-ios-swift/Podfile.lock +++ b/demo-ios-swift/Podfile.lock @@ -1,7 +1,7 @@ PODS: - - Google-Mobile-Ads-SDK (12.11.0): + - Google-Mobile-Ads-SDK (12.14.0): - GoogleUserMessagingPlatform (>= 1.1) - - GoogleUserMessagingPlatform (3.0.0) + - GoogleUserMessagingPlatform (3.1.0) - OptableSDK (0.10.0) DEPENDENCIES: @@ -18,10 +18,10 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - Google-Mobile-Ads-SDK: b833c723759e32bbaf06edaaf2293f08ed898232 - GoogleUserMessagingPlatform: f8d0cdad3ca835406755d0a69aa634f00e76d576 + Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2 + GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c OptableSDK: fc5d3852c29fac1881b1d3ab6ea397de71c8cbf1 -PODFILE CHECKSUM: a7bf67fad0ec61ca3a95f0f8a9aadf2c4fd2cd76 +PODFILE CHECKSUM: b1a602894bc694156e8907d24cabf069be3c8bee COCOAPODS: 1.16.2 diff --git a/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj b/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj index f4e17c3..a277065 100644 --- a/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj +++ b/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj @@ -15,29 +15,8 @@ 6352AA6224DC7AE9002E66EB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6352AA6024DC7AE9002E66EB /* Main.storyboard */; }; 6352AA6424DC7AEC002E66EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6352AA6324DC7AEC002E66EB /* Assets.xcassets */; }; 6352AA6724DC7AEC002E66EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6352AA6524DC7AEC002E66EB /* LaunchScreen.storyboard */; }; - 6352AA7224DC7AEC002E66EB /* demo_ios_swiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6352AA7124DC7AEC002E66EB /* demo_ios_swiftTests.swift */; }; - 6352AA7D24DC7AEC002E66EB /* demo_ios_swiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6352AA7C24DC7AEC002E66EB /* demo_ios_swiftUITests.swift */; }; - 8A63354DEF86B0AD26566B2E /* Pods_demo_ios_swiftTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A022CEF25F2CA644724048D /* Pods_demo_ios_swiftTests.framework */; }; - DD7B19462B7FBA17A7ED90D3 /* Pods_demo_ios_swift_demo_ios_swiftUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D56163CC808207FC0184D7A /* Pods_demo_ios_swift_demo_ios_swiftUITests.framework */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 6352AA6E24DC7AEC002E66EB /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6352AA4F24DC7AE9002E66EB /* Project object */; - proxyType = 1; - remoteGlobalIDString = 6352AA5624DC7AE9002E66EB; - remoteInfo = "demo-ios-swift"; - }; - 6352AA7924DC7AEC002E66EB /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6352AA4F24DC7AE9002E66EB /* Project object */; - proxyType = 1; - remoteGlobalIDString = 6352AA5624DC7AE9002E66EB; - remoteInfo = "demo-ios-swift"; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 63C1E32924EAD80B00C4FE51 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -63,12 +42,6 @@ 6352AA6324DC7AEC002E66EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6352AA6624DC7AEC002E66EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 6352AA6824DC7AEC002E66EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 6352AA6D24DC7AEC002E66EB /* demo-ios-swiftTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "demo-ios-swiftTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 6352AA7124DC7AEC002E66EB /* demo_ios_swiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = demo_ios_swiftTests.swift; sourceTree = ""; }; - 6352AA7324DC7AEC002E66EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 6352AA7824DC7AEC002E66EB /* demo-ios-swiftUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "demo-ios-swiftUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 6352AA7C24DC7AEC002E66EB /* demo_ios_swiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = demo_ios_swiftUITests.swift; sourceTree = ""; }; - 6352AA7E24DC7AEC002E66EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7D56163CC808207FC0184D7A /* Pods_demo_ios_swift_demo_ios_swiftUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_demo_ios_swift_demo_ios_swiftUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A909BB3E86CA17B56519FA37 /* Pods-demo-ios-swift.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-swift.debug.xcconfig"; path = "Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift.debug.xcconfig"; sourceTree = ""; }; AC9D63E534725E310399A3FF /* Pods-demo-ios-swift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-swift.release.xcconfig"; path = "Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift.release.xcconfig"; sourceTree = ""; }; @@ -87,22 +60,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 6352AA6A24DC7AEC002E66EB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 8A63354DEF86B0AD26566B2E /* Pods_demo_ios_swiftTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 6352AA7524DC7AEC002E66EB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - DD7B19462B7FBA17A7ED90D3 /* Pods_demo_ios_swift_demo_ios_swiftUITests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -110,8 +67,6 @@ isa = PBXGroup; children = ( 6352AA5924DC7AE9002E66EB /* demo-ios-swift */, - 6352AA7024DC7AEC002E66EB /* demo-ios-swiftTests */, - 6352AA7B24DC7AEC002E66EB /* demo-ios-swiftUITests */, 6352AA5824DC7AE9002E66EB /* Products */, 63C1E32324EAD73800C4FE51 /* Frameworks */, 8018ABD26BD652CFB14910C3 /* Pods */, @@ -122,8 +77,6 @@ isa = PBXGroup; children = ( 6352AA5724DC7AE9002E66EB /* demo-ios-swift.app */, - 6352AA6D24DC7AEC002E66EB /* demo-ios-swiftTests.xctest */, - 6352AA7824DC7AEC002E66EB /* demo-ios-swiftUITests.xctest */, ); name = Products; sourceTree = ""; @@ -143,24 +96,6 @@ path = "demo-ios-swift"; sourceTree = ""; }; - 6352AA7024DC7AEC002E66EB /* demo-ios-swiftTests */ = { - isa = PBXGroup; - children = ( - 6352AA7124DC7AEC002E66EB /* demo_ios_swiftTests.swift */, - 6352AA7324DC7AEC002E66EB /* Info.plist */, - ); - path = "demo-ios-swiftTests"; - sourceTree = ""; - }; - 6352AA7B24DC7AEC002E66EB /* demo-ios-swiftUITests */ = { - isa = PBXGroup; - children = ( - 6352AA7C24DC7AEC002E66EB /* demo_ios_swiftUITests.swift */, - 6352AA7E24DC7AEC002E66EB /* Info.plist */, - ); - path = "demo-ios-swiftUITests"; - sourceTree = ""; - }; 63C1E32324EAD73800C4FE51 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -208,46 +143,6 @@ productReference = 6352AA5724DC7AE9002E66EB /* demo-ios-swift.app */; productType = "com.apple.product-type.application"; }; - 6352AA6C24DC7AEC002E66EB /* demo-ios-swiftTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 6352AA8424DC7AEC002E66EB /* Build configuration list for PBXNativeTarget "demo-ios-swiftTests" */; - buildPhases = ( - 4A28A5F539FAA3BE3F696AEB /* [CP] Check Pods Manifest.lock */, - 6352AA6924DC7AEC002E66EB /* Sources */, - 6352AA6A24DC7AEC002E66EB /* Frameworks */, - 6352AA6B24DC7AEC002E66EB /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 6352AA6F24DC7AEC002E66EB /* PBXTargetDependency */, - ); - name = "demo-ios-swiftTests"; - productName = "demo-ios-swiftTests"; - productReference = 6352AA6D24DC7AEC002E66EB /* demo-ios-swiftTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 6352AA7724DC7AEC002E66EB /* demo-ios-swiftUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 6352AA8724DC7AEC002E66EB /* Build configuration list for PBXNativeTarget "demo-ios-swiftUITests" */; - buildPhases = ( - 20C5E6446887B9BB11EFA39B /* [CP] Check Pods Manifest.lock */, - 6352AA7424DC7AEC002E66EB /* Sources */, - 6352AA7524DC7AEC002E66EB /* Frameworks */, - 6352AA7624DC7AEC002E66EB /* Resources */, - A4AD3035FC184EEC8114DE25 /* [CP] Embed Pods Frameworks */, - D947CAD4B818D4D20E781F9D /* [CP] Copy Pods Resources */, - ); - buildRules = ( - ); - dependencies = ( - 6352AA7A24DC7AEC002E66EB /* PBXTargetDependency */, - ); - name = "demo-ios-swiftUITests"; - productName = "demo-ios-swiftUITests"; - productReference = 6352AA7824DC7AEC002E66EB /* demo-ios-swiftUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -261,14 +156,6 @@ 6352AA5624DC7AE9002E66EB = { CreatedOnToolsVersion = 11.6; }; - 6352AA6C24DC7AEC002E66EB = { - CreatedOnToolsVersion = 11.6; - TestTargetID = 6352AA5624DC7AE9002E66EB; - }; - 6352AA7724DC7AEC002E66EB = { - CreatedOnToolsVersion = 11.6; - TestTargetID = 6352AA5624DC7AE9002E66EB; - }; }; }; buildConfigurationList = 6352AA5224DC7AE9002E66EB /* Build configuration list for PBXProject "demo-ios-swift" */; @@ -285,8 +172,6 @@ projectRoot = ""; targets = ( 6352AA5624DC7AE9002E66EB /* demo-ios-swift */, - 6352AA6C24DC7AEC002E66EB /* demo-ios-swiftTests */, - 6352AA7724DC7AEC002E66EB /* demo-ios-swiftUITests */, ); }; /* End PBXProject section */ @@ -302,67 +187,9 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 6352AA6B24DC7AEC002E66EB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 6352AA7624DC7AEC002E66EB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 20C5E6446887B9BB11EFA39B /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-demo-ios-swift-demo-ios-swiftUITests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 4A28A5F539FAA3BE3F696AEB /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-demo-ios-swiftTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 967E82BE8040FB9C5D9DB711 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -385,23 +212,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - A4AD3035FC184EEC8114DE25 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift-demo-ios-swiftUITests/Pods-demo-ios-swift-demo-ios-swiftUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift-demo-ios-swiftUITests/Pods-demo-ios-swift-demo-ios-swiftUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift-demo-ios-swiftUITests/Pods-demo-ios-swift-demo-ios-swiftUITests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; CB6C39DB30FCBD8A10020CE8 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -419,23 +229,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - D947CAD4B818D4D20E781F9D /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift-demo-ios-swiftUITests/Pods-demo-ios-swift-demo-ios-swiftUITests-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift-demo-ios-swiftUITests/Pods-demo-ios-swift-demo-ios-swiftUITests-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift-demo-ios-swiftUITests/Pods-demo-ios-swift-demo-ios-swiftUITests-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; E4BF60F1588582695FC58696 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -467,37 +260,8 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 6352AA6924DC7AEC002E66EB /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 6352AA7224DC7AEC002E66EB /* demo_ios_swiftTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 6352AA7424DC7AEC002E66EB /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 6352AA7D24DC7AEC002E66EB /* demo_ios_swiftUITests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 6352AA6F24DC7AEC002E66EB /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 6352AA5624DC7AE9002E66EB /* demo-ios-swift */; - targetProxy = 6352AA6E24DC7AEC002E66EB /* PBXContainerItemProxy */; - }; - 6352AA7A24DC7AEC002E66EB /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 6352AA5624DC7AE9002E66EB /* demo-ios-swift */; - targetProxy = 6352AA7924DC7AEC002E66EB /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 6352AA6024DC7AE9002E66EB /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -670,90 +434,6 @@ }; name = Release; }; - 6352AA8524DC7AEC002E66EB /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = BEEBA234E54019EC38C70912 /* Pods-demo-ios-swiftTests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = "demo-ios-swiftTests/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-swiftTests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/demo-ios-swift.app/demo-ios-swift"; - }; - name = Debug; - }; - 6352AA8624DC7AEC002E66EB /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = AFE041B55E3398A6DDE84BFF /* Pods-demo-ios-swiftTests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = "demo-ios-swiftTests/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-swiftTests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/demo-ios-swift.app/demo-ios-swift"; - }; - name = Release; - }; - 6352AA8824DC7AEC002E66EB /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3386157F1FDF211F3621C6CF /* Pods-demo-ios-swift-demo-ios-swiftUITests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = "demo-ios-swiftUITests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-swiftUITests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = "demo-ios-swift"; - }; - name = Debug; - }; - 6352AA8924DC7AEC002E66EB /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = E8E21CA91246EF005B351B37 /* Pods-demo-ios-swift-demo-ios-swiftUITests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = "demo-ios-swiftUITests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-swiftUITests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = "demo-ios-swift"; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -775,24 +455,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 6352AA8424DC7AEC002E66EB /* Build configuration list for PBXNativeTarget "demo-ios-swiftTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 6352AA8524DC7AEC002E66EB /* Debug */, - 6352AA8624DC7AEC002E66EB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 6352AA8724DC7AEC002E66EB /* Build configuration list for PBXNativeTarget "demo-ios-swiftUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 6352AA8824DC7AEC002E66EB /* Debug */, - 6352AA8924DC7AEC002E66EB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 6352AA4F24DC7AE9002E66EB /* Project object */; diff --git a/demo-ios-swift/demo-ios-swift.xcodeproj/xcshareddata/xcschemes/demo-ios-swift.xcscheme b/demo-ios-swift/demo-ios-swift.xcodeproj/xcshareddata/xcschemes/demo-ios-swift.xcscheme new file mode 100644 index 0000000..fc820c6 --- /dev/null +++ b/demo-ios-swift/demo-ios-swift.xcodeproj/xcshareddata/xcschemes/demo-ios-swift.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo-ios-swift/demo-ios-swift/AppDelegate.swift b/demo-ios-swift/demo-ios-swift/AppDelegate.swift index 907de8b..b0f0c46 100644 --- a/demo-ios-swift/demo-ios-swift/AppDelegate.swift +++ b/demo-ios-swift/demo-ios-swift/AppDelegate.swift @@ -6,8 +6,8 @@ // See LICENSE for details. // -import UIKit import OptableSDK +import UIKit // The OPTABLE global points to an instance of OptableSDK which is initialized in the AppDelegate application() method at app launch. // While we could have initialized the global directly here, due to Swift lazy-loading this would delay initialization to the first @@ -16,14 +16,19 @@ import OptableSDK // ideally have init happen well before the first usage/API call if possible. var OPTABLE: OptableSDK? +// MARK: - AppDelegate @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - - // See comment further above on why we are initializing OptableSDK() from here: - OPTABLE = OptableSDK(host: "sandbox.optable.co", app: "ios-sdk-demo") + + let config = OptableConfig( + tenant: "prebidtest", + originSlug: "ios-sdk", + host: "prebidtest.cloud.optable.co", + skipAdvertisingIdDetection: false + ) + OPTABLE = OptableSDK(config: config) return true } diff --git a/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift b/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift index cd11738..46a0dd4 100644 --- a/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift +++ b/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift @@ -7,17 +7,17 @@ // import UIKit +import OptableSDK class IdentifyViewController: UIViewController { - // MARK: Properties - @IBOutlet weak var identifyInput: UITextField! - @IBOutlet weak var identifyButton: UIButton! - @IBOutlet weak var identifyIDFA: UISwitch! - @IBOutlet weak var identifyOutput: UITextView! + @IBOutlet var identifyInput: UITextField! + @IBOutlet var identifyButton: UIButton! + @IBOutlet var identifyIDFA: UISwitch! + @IBOutlet var identifyOutput: UITextView! // MARK: Actions - + // dispatchIdentify() is the action invoked on a click on the "Identify" UIButton in our demo app. It initiates // a call to the OptableSDK.identify() API and prints debugging information to the UI and debug console. @IBAction func dispatchIdentify(_ sender: UIButton) { @@ -26,26 +26,35 @@ class IdentifyViewController: UIViewController { let aaid = identifyIDFA.isOn as Bool identifyOutput.text = "Calling /identify API with:\n\n" - if (email != "") { + if email != "" { identifyOutput.text += "Email: " + email + "\n" } identifyOutput.text += "IDFA: " + String(aaid) + "\n" - try OPTABLE!.identify(email: email, aaid: aaid) { result in - switch result { - case .success(let response): - print("[OptableSDK] Success on /identify API call: response.statusCode = \(response.statusCode)") - DispatchQueue.main.async { - self.identifyOutput.text += "\n✅ Success." - } + let idfa = "06DE8C6A-A431-4235-A262-E3A9C2CCEB34" + let gaid = "D04BB8C3-5A3E-4964-9757-D38365F59E6A" + let phoneNumber = "+1234567890" + let custom = "new-custom.ABC" + let custom9 = "custom-9-id" - case .failure(let error): - print("[OptableSDK] Error on /identify API call: \(error)") - DispatchQueue.main.async { - self.identifyOutput.text += "\n🚫 Error: \(error)" + try OPTABLE!.identify( + OptableIdentifiers(emailAddress: email, phoneNumber: phoneNumber, appleIDFA: idfa, googleGAID: gaid, custom: ["c": custom, "c9": custom9]), + { result in + switch result { + case let .success(response): + print("[OptableSDK] Success on /identify API call: response.statusCode = \(response.statusCode)") + DispatchQueue.main.async { + self.identifyOutput.text += "\n✅ Success. Response: \(response)" + } + + case let .failure(error): + print("[OptableSDK] Error on /identify API call: \(error)") + DispatchQueue.main.async { + self.identifyOutput.text += "\n🚫 Error: \(error)" + } } } - } + ) } catch { print("[OptableSDK] Exception: \(error)") diff --git a/demo-ios-swift/demo-ios-swiftTests/Info.plist b/demo-ios-swift/demo-ios-swiftTests/Info.plist deleted file mode 100644 index 64d65ca..0000000 --- a/demo-ios-swift/demo-ios-swiftTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/demo-ios-swift/demo-ios-swiftTests/demo_ios_swiftTests.swift b/demo-ios-swift/demo-ios-swiftTests/demo_ios_swiftTests.swift deleted file mode 100644 index 09b78e2..0000000 --- a/demo-ios-swift/demo-ios-swiftTests/demo_ios_swiftTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// demo_ios_swiftTests.swift -// demo-ios-swiftTests -// -// Copyright © 2020 Optable Technologies Inc. All rights reserved. -// See LICENSE for details. -// - -import XCTest -@testable import demo_ios_swift - -class demo_ios_swiftTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/demo-ios-swift/demo-ios-swiftUITests/Info.plist b/demo-ios-swift/demo-ios-swiftUITests/Info.plist deleted file mode 100644 index 64d65ca..0000000 --- a/demo-ios-swift/demo-ios-swiftUITests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/demo-ios-swift/demo-ios-swiftUITests/demo_ios_swiftUITests.swift b/demo-ios-swift/demo-ios-swiftUITests/demo_ios_swiftUITests.swift deleted file mode 100644 index 342866f..0000000 --- a/demo-ios-swift/demo-ios-swiftUITests/demo_ios_swiftUITests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// demo_ios_swiftUITests.swift -// demo-ios-swiftUITests -// -// Copyright © 2020 Optable Technologies Inc. All rights reserved. -// See LICENSE for details. -// - -import XCTest - -class demo_ios_swiftUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { - XCUIApplication().launch() - } - } - } -} From b3c5e042885e08d726998713d3e73c47fa9f874d Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Thu, 18 Dec 2025 13:04:51 +0100 Subject: [PATCH 23/42] - Updated demo-ios-objc --- demo-ios-objc/Podfile | 11 +- demo-ios-objc/Podfile.lock | 10 +- .../demo-ios-objc.xcodeproj/project.pbxproj | 352 ------------------ .../xcschemes/demo-ios-objc.xcscheme | 85 +++++ demo-ios-objc/demo-ios-objc/AppDelegate.m | 10 +- .../demo-ios-objc/IdentifyViewController.m | 26 +- .../demo-ios-objc/OptableSDKDelegate.m | 32 +- demo-ios-objc/demo-ios-objcTests/Info.plist | 22 -- .../demo-ios-objcTests/demo_ios_objcTests.m | 37 -- demo-ios-objc/demo-ios-objcUITests/Info.plist | 22 -- .../demo_ios_objcUITests.m | 48 --- 11 files changed, 133 insertions(+), 522 deletions(-) create mode 100644 demo-ios-objc/demo-ios-objc.xcodeproj/xcshareddata/xcschemes/demo-ios-objc.xcscheme delete mode 100644 demo-ios-objc/demo-ios-objcTests/Info.plist delete mode 100644 demo-ios-objc/demo-ios-objcTests/demo_ios_objcTests.m delete mode 100644 demo-ios-objc/demo-ios-objcUITests/Info.plist delete mode 100644 demo-ios-objc/demo-ios-objcUITests/demo_ios_objcUITests.m diff --git a/demo-ios-objc/Podfile b/demo-ios-objc/Podfile index 71f6808..c1a71cc 100644 --- a/demo-ios-objc/Podfile +++ b/demo-ios-objc/Podfile @@ -1,4 +1,4 @@ -platform :ios, '14.0' +platform :ios, '15.0' source 'https://cdn.cocoapods.org/' @@ -13,13 +13,4 @@ target 'demo-ios-objc' do pod 'Google-Mobile-Ads-SDK' - target 'demo-ios-objcTests' do - inherit! :search_paths - # Pods for testing - end - - target 'demo-ios-objcUITests' do - # Pods for testing - end - end diff --git a/demo-ios-objc/Podfile.lock b/demo-ios-objc/Podfile.lock index 2fb5c4b..dc3bd83 100644 --- a/demo-ios-objc/Podfile.lock +++ b/demo-ios-objc/Podfile.lock @@ -1,7 +1,7 @@ PODS: - - Google-Mobile-Ads-SDK (12.12.0): + - Google-Mobile-Ads-SDK (12.14.0): - GoogleUserMessagingPlatform (>= 1.1) - - GoogleUserMessagingPlatform (3.0.0) + - GoogleUserMessagingPlatform (3.1.0) - OptableSDK (0.10.0) DEPENDENCIES: @@ -18,10 +18,10 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - Google-Mobile-Ads-SDK: 4dde70a8c18d96b14f9548759b8cec6ecb0bc3e6 - GoogleUserMessagingPlatform: f8d0cdad3ca835406755d0a69aa634f00e76d576 + Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2 + GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c OptableSDK: fc5d3852c29fac1881b1d3ab6ea397de71c8cbf1 -PODFILE CHECKSUM: 48d927338b39550c29272b694f6c18710f33f913 +PODFILE CHECKSUM: b0f8f6f1358c223af6d51da031c3c1e93278a090 COCOAPODS: 1.16.2 diff --git a/demo-ios-objc/demo-ios-objc.xcodeproj/project.pbxproj b/demo-ios-objc/demo-ios-objc.xcodeproj/project.pbxproj index c53bf04..deefd94 100644 --- a/demo-ios-objc/demo-ios-objc.xcodeproj/project.pbxproj +++ b/demo-ios-objc/demo-ios-objc.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 10F4BF742EFDE899E4AE482F /* Pods_demo_ios_objc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 35D48072B3533132ACEA0D21 /* Pods_demo_ios_objc.framework */; }; - 11D09485910F1A1EB9871E8E /* Pods_demo_ios_objcTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB5262E52B5F4397F9533E61 /* Pods_demo_ios_objcTests.framework */; }; 6320EEFE2535F92300F76877 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6320EEFD2535F92300F76877 /* AppDelegate.m */; }; 6320EF012535F92300F76877 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6320EF002535F92300F76877 /* SceneDelegate.m */; }; 6320EF042535F92300F76877 /* IdentifyViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6320EF032535F92300F76877 /* IdentifyViewController.m */; }; @@ -16,32 +15,12 @@ 6320EF092535F92500F76877 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6320EF082535F92500F76877 /* Assets.xcassets */; }; 6320EF0C2535F92500F76877 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6320EF0A2535F92500F76877 /* LaunchScreen.storyboard */; }; 6320EF0F2535F92500F76877 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6320EF0E2535F92500F76877 /* main.m */; }; - 6320EF192535F92600F76877 /* demo_ios_objcTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6320EF182535F92600F76877 /* demo_ios_objcTests.m */; }; - 6320EF242535F92600F76877 /* demo_ios_objcUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6320EF232535F92600F76877 /* demo_ios_objcUITests.m */; }; 63B5A8C125366FE8000CA436 /* GAMBannerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 63B5A8C025366FE8000CA436 /* GAMBannerViewController.m */; }; 63B5A91A25368252000CA436 /* GAMBannerViewController.h in Sources */ = {isa = PBXBuildFile; fileRef = 63B5A8C52536704F000CA436 /* GAMBannerViewController.h */; }; 63B5A91E2536825A000CA436 /* IdentifyViewController.h in Sources */ = {isa = PBXBuildFile; fileRef = 6320EF022535F92300F76877 /* IdentifyViewController.h */; }; 63B5AAE3253E4047000CA436 /* OptableSDKDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 63B5AAE2253E4047000CA436 /* OptableSDKDelegate.m */; }; - 6B3F5A6A9D5ACACB900C3F8D /* Pods_demo_ios_objc_demo_ios_objcUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C562AA9ABB1162DB4C0814E /* Pods_demo_ios_objc_demo_ios_objcUITests.framework */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 6320EF152535F92600F76877 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6320EEF12535F92300F76877 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 6320EEF82535F92300F76877; - remoteInfo = "demo-ios-objc"; - }; - 6320EF202535F92600F76877 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6320EEF12535F92300F76877 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 6320EEF82535F92300F76877; - remoteInfo = "demo-ios-objc"; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXFileReference section */ 227B8E60A0CD66C45A87270D /* Pods-demo-ios-objcTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-objcTests.release.xcconfig"; path = "Target Support Files/Pods-demo-ios-objcTests/Pods-demo-ios-objcTests.release.xcconfig"; sourceTree = ""; }; 35D48072B3533132ACEA0D21 /* Pods_demo_ios_objc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_demo_ios_objc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -60,12 +39,6 @@ 6320EF0B2535F92500F76877 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 6320EF0D2535F92500F76877 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 6320EF0E2535F92500F76877 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 6320EF142535F92600F76877 /* demo-ios-objcTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "demo-ios-objcTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 6320EF182535F92600F76877 /* demo_ios_objcTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = demo_ios_objcTests.m; sourceTree = ""; }; - 6320EF1A2535F92600F76877 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 6320EF1F2535F92600F76877 /* demo-ios-objcUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "demo-ios-objcUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 6320EF232535F92600F76877 /* demo_ios_objcUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = demo_ios_objcUITests.m; sourceTree = ""; }; - 6320EF252535F92600F76877 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 63B5A8C025366FE8000CA436 /* GAMBannerViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GAMBannerViewController.m; sourceTree = ""; }; 63B5A8C52536704F000CA436 /* GAMBannerViewController.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; fileEncoding = 4; path = GAMBannerViewController.h; sourceTree = ""; }; 63B5AAE2253E4047000CA436 /* OptableSDKDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OptableSDKDelegate.m; sourceTree = ""; }; @@ -85,22 +58,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 6320EF112535F92600F76877 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 11D09485910F1A1EB9871E8E /* Pods_demo_ios_objcTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 6320EF1C2535F92600F76877 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 6B3F5A6A9D5ACACB900C3F8D /* Pods_demo_ios_objc_demo_ios_objcUITests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -118,8 +75,6 @@ isa = PBXGroup; children = ( 6320EEFB2535F92300F76877 /* demo-ios-objc */, - 6320EF172535F92600F76877 /* demo-ios-objcTests */, - 6320EF222535F92600F76877 /* demo-ios-objcUITests */, 6320EEFA2535F92300F76877 /* Products */, 689016CDEAF48669106E413E /* Pods */, 2CA740F147AE15FDA936899B /* Frameworks */, @@ -130,8 +85,6 @@ isa = PBXGroup; children = ( 6320EEF92535F92300F76877 /* demo-ios-objc.app */, - 6320EF142535F92600F76877 /* demo-ios-objcTests.xctest */, - 6320EF1F2535F92600F76877 /* demo-ios-objcUITests.xctest */, ); name = Products; sourceTree = ""; @@ -158,24 +111,6 @@ path = "demo-ios-objc"; sourceTree = ""; }; - 6320EF172535F92600F76877 /* demo-ios-objcTests */ = { - isa = PBXGroup; - children = ( - 6320EF182535F92600F76877 /* demo_ios_objcTests.m */, - 6320EF1A2535F92600F76877 /* Info.plist */, - ); - path = "demo-ios-objcTests"; - sourceTree = ""; - }; - 6320EF222535F92600F76877 /* demo-ios-objcUITests */ = { - isa = PBXGroup; - children = ( - 6320EF232535F92600F76877 /* demo_ios_objcUITests.m */, - 6320EF252535F92600F76877 /* Info.plist */, - ); - path = "demo-ios-objcUITests"; - sourceTree = ""; - }; 689016CDEAF48669106E413E /* Pods */ = { isa = PBXGroup; children = ( @@ -212,46 +147,6 @@ productReference = 6320EEF92535F92300F76877 /* demo-ios-objc.app */; productType = "com.apple.product-type.application"; }; - 6320EF132535F92600F76877 /* demo-ios-objcTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 6320EF2B2535F92600F76877 /* Build configuration list for PBXNativeTarget "demo-ios-objcTests" */; - buildPhases = ( - 89A14972C68A56F7258FFCEE /* [CP] Check Pods Manifest.lock */, - 6320EF102535F92600F76877 /* Sources */, - 6320EF112535F92600F76877 /* Frameworks */, - 6320EF122535F92600F76877 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 6320EF162535F92600F76877 /* PBXTargetDependency */, - ); - name = "demo-ios-objcTests"; - productName = "demo-ios-objcTests"; - productReference = 6320EF142535F92600F76877 /* demo-ios-objcTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 6320EF1E2535F92600F76877 /* demo-ios-objcUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 6320EF2E2535F92600F76877 /* Build configuration list for PBXNativeTarget "demo-ios-objcUITests" */; - buildPhases = ( - 6D4ACEF9C8A68F2C037DB6B2 /* [CP] Check Pods Manifest.lock */, - 6320EF1B2535F92600F76877 /* Sources */, - 6320EF1C2535F92600F76877 /* Frameworks */, - 6320EF1D2535F92600F76877 /* Resources */, - AB4697C1A666D79358A1D434 /* [CP] Embed Pods Frameworks */, - BBD55B57C4C9D1BE96E25EA6 /* [CP] Copy Pods Resources */, - ); - buildRules = ( - ); - dependencies = ( - 6320EF212535F92600F76877 /* PBXTargetDependency */, - ); - name = "demo-ios-objcUITests"; - productName = "demo-ios-objcUITests"; - productReference = 6320EF1F2535F92600F76877 /* demo-ios-objcUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -264,14 +159,6 @@ CreatedOnToolsVersion = 12.0.1; LastSwiftMigration = 1200; }; - 6320EF132535F92600F76877 = { - CreatedOnToolsVersion = 12.0.1; - TestTargetID = 6320EEF82535F92300F76877; - }; - 6320EF1E2535F92600F76877 = { - CreatedOnToolsVersion = 12.0.1; - TestTargetID = 6320EEF82535F92300F76877; - }; }; }; buildConfigurationList = 6320EEF42535F92300F76877 /* Build configuration list for PBXProject "demo-ios-objc" */; @@ -288,8 +175,6 @@ projectRoot = ""; targets = ( 6320EEF82535F92300F76877 /* demo-ios-objc */, - 6320EF132535F92600F76877 /* demo-ios-objcTests */, - 6320EF1E2535F92600F76877 /* demo-ios-objcUITests */, ); }; /* End PBXProject section */ @@ -305,20 +190,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 6320EF122535F92600F76877 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 6320EF1D2535F92600F76877 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -352,63 +223,15 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 6D4ACEF9C8A68F2C037DB6B2 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-demo-ios-objc-demo-ios-objcUITests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 89A14972C68A56F7258FFCEE /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-demo-ios-objcTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; A3A695A26763A7F969BB5E1A /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -417,61 +240,15 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc-resources.sh\"\n"; showEnvVarsInLog = 0; }; - AB4697C1A666D79358A1D434 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc-demo-ios-objcUITests/Pods-demo-ios-objc-demo-ios-objcUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc-demo-ios-objcUITests/Pods-demo-ios-objc-demo-ios-objcUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc-demo-ios-objcUITests/Pods-demo-ios-objc-demo-ios-objcUITests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - BBD55B57C4C9D1BE96E25EA6 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc-demo-ios-objcUITests/Pods-demo-ios-objc-demo-ios-objcUITests-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc-demo-ios-objcUITests/Pods-demo-ios-objc-demo-ios-objcUITests-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc-demo-ios-objcUITests/Pods-demo-ios-objc-demo-ios-objcUITests-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -490,37 +267,8 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 6320EF102535F92600F76877 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 6320EF192535F92600F76877 /* demo_ios_objcTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 6320EF1B2535F92600F76877 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 6320EF242535F92600F76877 /* demo_ios_objcUITests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 6320EF162535F92600F76877 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 6320EEF82535F92300F76877 /* demo-ios-objc */; - targetProxy = 6320EF152535F92600F76877 /* PBXContainerItemProxy */; - }; - 6320EF212535F92600F76877 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 6320EEF82535F92300F76877 /* demo-ios-objc */; - targetProxy = 6320EF202535F92600F76877 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 6320EF052535F92300F76877 /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -700,88 +448,6 @@ }; name = Release; }; - 6320EF2C2535F92600F76877 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 83DB069E12658B3EA13B8867 /* Pods-demo-ios-objcTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "demo-ios-objcTests/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-objcTests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/demo-ios-objc.app/demo-ios-objc"; - }; - name = Debug; - }; - 6320EF2D2535F92600F76877 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 227B8E60A0CD66C45A87270D /* Pods-demo-ios-objcTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "demo-ios-objcTests/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-objcTests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/demo-ios-objc.app/demo-ios-objc"; - }; - name = Release; - }; - 6320EF2F2535F92600F76877 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 45728C7558D469A46F00466E /* Pods-demo-ios-objc-demo-ios-objcUITests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "demo-ios-objcUITests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-objcUITests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = "demo-ios-objc"; - }; - name = Debug; - }; - 6320EF302535F92600F76877 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 523A0EFF3A972FC18833C15D /* Pods-demo-ios-objc-demo-ios-objcUITests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "demo-ios-objcUITests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-objcUITests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = "demo-ios-objc"; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -803,24 +469,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 6320EF2B2535F92600F76877 /* Build configuration list for PBXNativeTarget "demo-ios-objcTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 6320EF2C2535F92600F76877 /* Debug */, - 6320EF2D2535F92600F76877 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 6320EF2E2535F92600F76877 /* Build configuration list for PBXNativeTarget "demo-ios-objcUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 6320EF2F2535F92600F76877 /* Debug */, - 6320EF302535F92600F76877 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 6320EEF12535F92300F76877 /* Project object */; diff --git a/demo-ios-objc/demo-ios-objc.xcodeproj/xcshareddata/xcschemes/demo-ios-objc.xcscheme b/demo-ios-objc/demo-ios-objc.xcodeproj/xcshareddata/xcschemes/demo-ios-objc.xcscheme new file mode 100644 index 0000000..deabb2f --- /dev/null +++ b/demo-ios-objc/demo-ios-objc.xcodeproj/xcshareddata/xcschemes/demo-ios-objc.xcscheme @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo-ios-objc/demo-ios-objc/AppDelegate.m b/demo-ios-objc/demo-ios-objc/AppDelegate.m index 3a90b4b..b49c0b9 100644 --- a/demo-ios-objc/demo-ios-objc/AppDelegate.m +++ b/demo-ios-objc/demo-ios-objc/AppDelegate.m @@ -20,10 +20,14 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. - OPTABLE = [[OptableSDK alloc] initWithHost: @"sandbox.optable.co" app: @"ios-sdk-demo" insecure: NO useragent: nil]; - OptableSDKDelegate *delegate = [[OptableSDKDelegate alloc] init]; + OptableSDKDelegate *delegate = [OptableSDKDelegate new]; + + OptableConfig *config = [[OptableConfig alloc] initWithTenant: @"prebidtest" originSlug: @"ios-sdk"]; + config.host = @"prebidtest.cloud.optable.co"; + + OPTABLE = [[OptableSDK alloc] initWithConfig: config]; OPTABLE.delegate = delegate; - + return YES; } diff --git a/demo-ios-objc/demo-ios-objc/IdentifyViewController.m b/demo-ios-objc/demo-ios-objc/IdentifyViewController.m index e046adb..d16c7a5 100644 --- a/demo-ios-objc/demo-ios-objc/IdentifyViewController.m +++ b/demo-ios-objc/demo-ios-objc/IdentifyViewController.m @@ -18,25 +18,25 @@ @implementation IdentifyViewController - (void)viewDidLoad { [super viewDidLoad]; - + OptableSDKDelegate *delegate = (OptableSDKDelegate *)OPTABLE.delegate; delegate.identifyOutput = self.identifyOutput; } - (IBAction)dispatchIdentify:(id)sender { - NSString *email = [_identifyInput text]; - bool aaid = [_identifyIDFA isOn]; - NSMutableString *output; - NSError *error = nil; - - output = [NSMutableString stringWithFormat:@"Calling /identify API with:\n\n"]; - if ([email length] > 0) { - [output appendString:[NSString stringWithFormat:@"Email: %@\n", email]]; + NSString *email = _identifyInput.text; + BOOL aaid = _identifyIDFA.isOn; + + NSMutableString *output = [NSMutableString stringWithFormat: @"Calling /identify API with:\n\n"]; + if (email.length > 0) { + [output appendString: [NSString stringWithFormat: @"Email: %@\n", email]]; } - [output appendString:[NSString stringWithFormat:@"IDFA: %s\n", aaid ? "true" : "false"]]; - [_identifyOutput setText:output]; - - [OPTABLE identify :email aaid:aaid ppid:@"" error:&error]; + [output appendString: [NSString stringWithFormat: @"IDFA: %s\n", aaid ? "true" : "false"]]; + _identifyOutput.text = output; + + NSError *error = nil; + NSDictionary *ids = @{ @"e" : email, @"c" : @"" }; + [OPTABLE identify: ids error:&error]; } @end diff --git a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m index 67e0774..5633774 100644 --- a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m +++ b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m @@ -16,28 +16,34 @@ @interface OptableSDKDelegate () @implementation OptableSDKDelegate - (void)identifyOk:(NSHTTPURLResponse *)result { NSLog(@"[OptableSDK] Success on /identify API call. HTTP Status Code: %ld", result.statusCode); + dispatch_async(dispatch_get_main_queue(), ^{ - [self.identifyOutput setText:[NSString stringWithFormat:@"%@\n✅ Success", [self.identifyOutput text]]]; + NSString *output = [NSString stringWithFormat: @"%@\n✅ Success", self.identifyOutput.text]; + self.identifyOutput.text = output; }); } - (void)identifyErr:(NSError *)error { NSLog(@"[OptableSDK] Error on /identify API call: %@", [error localizedDescription]); + dispatch_async(dispatch_get_main_queue(), ^{ - [self.identifyOutput setText:[NSString stringWithFormat:@"%@\n🚫 Error: %@\n", [self.identifyOutput text], [error localizedDescription]]]; + NSString *output = [NSString stringWithFormat: @"%@\n🚫 Error: %@\n", self.identifyOutput.text, error.localizedDescription]; + self.identifyOutput.text = output; }); } - (void)profileOk:(NSHTTPURLResponse *)result { NSLog(@"[OptableSDK] Success on /profile API call. HTTP Status Code: %ld", result.statusCode); dispatch_async(dispatch_get_main_queue(), ^{ - [self.targetingOutput setText:[NSString stringWithFormat:@"%@\n✅ Success calling profile API to set example traits.\n", [self.targetingOutput text]]]; + NSString* output = [NSString stringWithFormat: @"%@\n✅ Success calling profile API to set example traits.\n", self.targetingOutput.text]; + self.targetingOutput.text = output; }); } - (void)profileErr:(NSError *)error { NSLog(@"[OptableSDK] Error on /profile API call: %@", [error localizedDescription]); dispatch_async(dispatch_get_main_queue(), ^{ - [self.targetingOutput setText:[NSString stringWithFormat:@"%@\n🚫 Error: %@\n", [self.targetingOutput text], [error localizedDescription]]]; + NSString* output = [NSString stringWithFormat: @"%@\n🚫 Error: %@\n", self.targetingOutput.text, error.localizedDescription]; + self.targetingOutput.text = output; }); } - (void)targetingOk:(NSDictionary *)result { @@ -45,34 +51,40 @@ - (void)targetingOk:(NSDictionary *)result { GAMRequest *request = [GAMRequest request]; request.customTargeting = result; [self.bannerView loadRequest:request]; - + NSLog(@"[OptableSDK] Success on /targeting API call: %@", result); + dispatch_async(dispatch_get_main_queue(), ^{ - [self.targetingOutput setText:[NSString stringWithFormat:@"%@\nData: %@\n", [self.targetingOutput text], result]]; + NSString* output = [NSString stringWithFormat: @"%@\nData: %@\n", self.targetingOutput.text, result]; + self.targetingOutput.text = output; }); } - (void)targetingErr:(NSError *)error { // Update the GAM banner view without targeting data: GAMRequest *request = [GAMRequest request]; [self.bannerView loadRequest:request]; - + NSLog(@"[OptableSDK] Error on /targeting API call: %@", [error localizedDescription]); + dispatch_async(dispatch_get_main_queue(), ^{ - [self.targetingOutput setText:[NSString stringWithFormat:@"%@\n🚫 Error: %@\n", [self.targetingOutput text], [error localizedDescription]]]; + NSString* output = [NSString stringWithFormat: @"%@\n🚫 Error: %@\n", self.targetingOutput.text, error.localizedDescription]; + self.targetingOutput.text = output; }); } - (void)witnessOk:(NSHTTPURLResponse *)result { NSLog(@"[OptableSDK] Success on /witness API call. HTTP Status Code: %ld", result.statusCode); dispatch_async(dispatch_get_main_queue(), ^{ - [self.targetingOutput setText:[NSString stringWithFormat:@"%@\n✅ Success calling witness API to log loadBannerClicked event.\n", [self.targetingOutput text]]]; + NSString* output = [NSString stringWithFormat: @"%@\n✅ Success calling witness API to log loadBannerClicked event.\n", self.targetingOutput.text]; + self.targetingOutput.text = output; }); } - (void)witnessErr:(NSError *)error { NSLog(@"[OptableSDK] Error on /witness API call: %@", [error localizedDescription]); dispatch_async(dispatch_get_main_queue(), ^{ - [self.targetingOutput setText:[NSString stringWithFormat:@"%@\n🚫 Error: %@\n", [self.targetingOutput text], [error localizedDescription]]]; + NSString* output = [NSString stringWithFormat:@"%@\n🚫 Error: %@\n", self.targetingOutput.text, error.localizedDescription]; + self.targetingOutput.text = output; }); } @end diff --git a/demo-ios-objc/demo-ios-objcTests/Info.plist b/demo-ios-objc/demo-ios-objcTests/Info.plist deleted file mode 100644 index 64d65ca..0000000 --- a/demo-ios-objc/demo-ios-objcTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/demo-ios-objc/demo-ios-objcTests/demo_ios_objcTests.m b/demo-ios-objc/demo-ios-objcTests/demo_ios_objcTests.m deleted file mode 100644 index 4d12736..0000000 --- a/demo-ios-objc/demo-ios-objcTests/demo_ios_objcTests.m +++ /dev/null @@ -1,37 +0,0 @@ -// -// demo_ios_objcTests.m -// demo-ios-objcTests -// -// Copyright © 2020 Optable Technologies Inc. All rights reserved. -// See LICENSE for details. -// - -#import - -@interface demo_ios_objcTests : XCTestCase - -@end - -@implementation demo_ios_objcTests - -- (void)setUp { - // Put setup code here. This method is called before the invocation of each test method in the class. -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. -} - -- (void)testExample { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. -} - -- (void)testPerformanceExample { - // This is an example of a performance test case. - [self measureBlock:^{ - // Put the code you want to measure the time of here. - }]; -} - -@end diff --git a/demo-ios-objc/demo-ios-objcUITests/Info.plist b/demo-ios-objc/demo-ios-objcUITests/Info.plist deleted file mode 100644 index 64d65ca..0000000 --- a/demo-ios-objc/demo-ios-objcUITests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/demo-ios-objc/demo-ios-objcUITests/demo_ios_objcUITests.m b/demo-ios-objc/demo-ios-objcUITests/demo_ios_objcUITests.m deleted file mode 100644 index 7ffebcc..0000000 --- a/demo-ios-objc/demo-ios-objcUITests/demo_ios_objcUITests.m +++ /dev/null @@ -1,48 +0,0 @@ -// -// demo_ios_objcUITests.m -// demo-ios-objcUITests -// -// Copyright © 2020 Optable Technologies Inc. All rights reserved. -// See LICENSE for details. -// - -#import - -@interface demo_ios_objcUITests : XCTestCase - -@end - -@implementation demo_ios_objcUITests - -- (void)setUp { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - self.continueAfterFailure = NO; - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. -} - -- (void)testExample { - // UI tests must launch the application that they test. - XCUIApplication *app = [[XCUIApplication alloc] init]; - [app launch]; - - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. -} - -- (void)testLaunchPerformance { - if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { - // This measures how long it takes to launch your application. - [self measureWithMetrics:@[[[XCTApplicationLaunchMetric alloc] init]] block:^{ - [[[XCUIApplication alloc] init] launch]; - }]; - } -} - -@end From 2c7e75e1db6786f57e8e3a00e995302713dbe1c3 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Thu, 18 Dec 2025 13:18:42 +0100 Subject: [PATCH 24/42] - Added empty ids check --- Source/OptableIdentifiers.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/OptableIdentifiers.swift b/Source/OptableIdentifiers.swift index 639f1ed..f1b347e 100644 --- a/Source/OptableIdentifiers.swift +++ b/Source/OptableIdentifiers.swift @@ -73,7 +73,10 @@ public struct OptableIdentifiers { var results: [String] = [] for (key, value) in dict { - guard let optableIdentifier = OptableIdentifierType(rawValue: key) else { continue } + guard + value.isEmpty == false, // skip empty values + let optableIdentifier = OptableIdentifierType(rawValue: key) + else { continue } let eid: String = switch optableIdentifier { case .emailAddress: OptableIdentifierEncoder.email(value) From 5f4e8a24864cc031674052f284ec9e67435c89e5 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Thu, 18 Dec 2025 15:16:23 +0100 Subject: [PATCH 25/42] - Minor fix --- Source/OptableSDK.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index 0b9f78d..3b416aa 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -225,7 +225,7 @@ public extension OptableSDK { Instead of completion callbacks, delegate methods are called. */ @objc - func witness(_ event: String, properties: NSDictionary) throws { + func witness(event: String, properties: NSDictionary) throws { try self.witness(event: event, properties: properties) { result in switch result { case let .success(response): From ae203280a9cd48b4cace0b0c3fba9bb21a6d0fde Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Thu, 18 Dec 2025 15:32:44 +0100 Subject: [PATCH 26/42] - Updated demo-ios-objc: added custom-param --- demo-ios-objc/demo-ios-objc/IdentifyViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-ios-objc/demo-ios-objc/IdentifyViewController.m b/demo-ios-objc/demo-ios-objc/IdentifyViewController.m index d16c7a5..37ec37e 100644 --- a/demo-ios-objc/demo-ios-objc/IdentifyViewController.m +++ b/demo-ios-objc/demo-ios-objc/IdentifyViewController.m @@ -35,7 +35,7 @@ - (IBAction)dispatchIdentify:(id)sender { _identifyOutput.text = output; NSError *error = nil; - NSDictionary *ids = @{ @"e" : email, @"c" : @"" }; + NSDictionary *ids = @{ @"e" : email, @"c" : @"new-custom.ABC" }; [OPTABLE identify: ids error:&error]; } From c24bcbe4b08148796a61f1daa491f7a6b0e3132c Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Thu, 18 Dec 2025 15:33:34 +0100 Subject: [PATCH 27/42] - Updated Tests: integration & unit --- OptableSDK.xcodeproj/project.pbxproj | 10 +- Tests/Integration/OptableSDKTests.swift | 181 ++++++++++++++++++ Tests/OptableSDKTests.swift | 40 ---- Tests/{ => Unit}/EdgeAPITests.swift | 0 .../OptableIdentifierEncoderTests.swift | 2 +- .../{ => Unit}/OptableIdentifiersTests.swift | 5 + 6 files changed, 193 insertions(+), 45 deletions(-) create mode 100644 Tests/Integration/OptableSDKTests.swift delete mode 100644 Tests/OptableSDKTests.swift rename Tests/{ => Unit}/EdgeAPITests.swift (100%) rename Tests/{ => Unit}/OptableIdentifierEncoderTests.swift (99%) rename Tests/{ => Unit}/OptableIdentifiersTests.swift (96%) diff --git a/OptableSDK.xcodeproj/project.pbxproj b/OptableSDK.xcodeproj/project.pbxproj index ce464d5..f5b1e62 100644 --- a/OptableSDK.xcodeproj/project.pbxproj +++ b/OptableSDK.xcodeproj/project.pbxproj @@ -39,11 +39,11 @@ CEBE038D2EF03ADD0027D67F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - EdgeAPITests.swift, + Integration/OptableSDKTests.swift, Misc/cartesianProduct.swift, - OptableIdentifierEncoderTests.swift, - OptableIdentifiersTests.swift, - OptableSDKTests.swift, + Unit/EdgeAPITests.swift, + Unit/OptableIdentifierEncoderTests.swift, + Unit/OptableIdentifiersTests.swift, ); target = 6352AB0324EAD403002E66EB /* OptableSDKTests */; }; @@ -423,6 +423,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -443,6 +444,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Tests/Integration/OptableSDKTests.swift b/Tests/Integration/OptableSDKTests.swift new file mode 100644 index 0000000..3bfc90d --- /dev/null +++ b/Tests/Integration/OptableSDKTests.swift @@ -0,0 +1,181 @@ +// +// OptableSDKTests.swift +// OptableSDKTests +// +// Copyright © 2020 Optable Technologies Inc. All rights reserved. +// See LICENSE for details. +// + +@testable import OptableSDK +import XCTest + +// MARK: - OptableSDKTests +class OptableSDKTests: XCTestCase { + let defaultConfig = OptableConfig(tenant: "prebidtest", originSlug: "ios-sdk", insecure: false, customUserAgent: "ios-integration-tests") + lazy var sdk = OptableSDK(config: defaultConfig) + + lazy var identifyExpectation = expectation(description: "identify-delegate-expectation") + lazy var targetExpectation = expectation(description: "target-delegate-expectation") + lazy var witnessExpectation = expectation(description: "witness-delegate-expectation") + lazy var profileExpectation = expectation(description: "profile-delegate-expectation") + + override func setUpWithError() throws { + sdk.delegate = self + } + + // MARK: Identify + @available(iOS 13.0, *) + func test_identify_async() async throws { + let response = try await sdk.identify(OptableIdentifiers(emailAddress: "test@test.com")) + XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor")) + XCTAssert(response.statusCode == 200) + } + + func test_identify_callback() throws { + let expectation = expectation(description: "identify-callback-expectation") + try sdk.identify(OptableIdentifiers(emailAddress: "test@test.com")) { result in + switch result { + case let .success(response): + XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor")) + XCTAssert(response.statusCode == 200) + case let .failure(failure): + XCTFail("Expected success, got error: \(failure)") + } + expectation.fulfill() + } + wait(for: [expectation], timeout: 10) + } + + func test_identify_delegate() throws { + try sdk.identify(["e": "test@test.com"]) + wait(for: [identifyExpectation], timeout: 10) + } + + // MARK: Target + @available(iOS 13.0, *) + func test_target_async() async throws { + let response: NSDictionary = try await sdk.targeting() + XCTAssert(response.allKeys.isEmpty == false) + } + + func test_target_callback() throws { + let expectation = expectation(description: "target-callback-expectation") + try sdk.targeting(completion: { result in + switch result { + case let .success(response): + XCTAssert(response.allKeys.isEmpty == false) + case let .failure(failure): + XCTFail("Expected success, got error: \(failure)") + } + expectation.fulfill() + }) + wait(for: [expectation], timeout: 10) + } + + func test_target_delegate() throws { + try sdk.targeting() + wait(for: [targetExpectation], timeout: 10) + } + + // MARK: Witness + @available(iOS 13.0, *) + func test_witness_async() async throws { + let response: HTTPURLResponse = try await sdk.witness(event: "test", properties: ["integration-test-witness": "integration-test-witness-value"]) + XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor")) + XCTAssert(response.statusCode == 200) + } + + 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 + switch result { + case let .success(response): + XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor")) + XCTAssert(response.statusCode == 200) + case let .failure(failure): + XCTFail("Expected success, got error: \(failure)") + } + expectation.fulfill() + }) + wait(for: [expectation], timeout: 10) + } + + func test_witness_delegate() throws { + try sdk.witness(event: "test", properties: ["integration-test-witness": "integration-test-witness-value"]) + wait(for: [witnessExpectation], timeout: 10) + } + + // MARK: Profile + @available(iOS 13.0, *) + func test_profile_async() async throws { + let response: HTTPURLResponse = try await sdk.profile(traits: ["integration-test-profile": "integration-test-profile-value"]) + XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor")) + XCTAssert(response.statusCode == 200) + } + + 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 + switch result { + case let .success(response): + XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor")) + XCTAssert(response.statusCode == 200) + case let .failure(failure): + XCTFail("Expected success, got error: \(failure)") + } + expectation.fulfill() + }) + wait(for: [expectation], timeout: 10) + } + + func test_profile_delegate() throws { + try sdk.profile(traits: ["integration-test-profile": "integration-test-profile-value"]) + wait(for: [profileExpectation], timeout: 10) + } +} + +// MARK: - OptableDelegate +extension OptableSDKTests: OptableDelegate { + func identifyOk(_ result: HTTPURLResponse) { + XCTAssert(result.allHeaderFields.keys.contains("x-optable-visitor")) + XCTAssert(result.statusCode == 200) + identifyExpectation.fulfill() + } + + func identifyErr(_ error: NSError) { + XCTFail("Expected success, got error: \(error)") + identifyExpectation.fulfill() + } + + func profileOk(_ result: HTTPURLResponse) { + XCTAssert(result.allHeaderFields.keys.contains("x-optable-visitor")) + XCTAssert(result.statusCode == 200) + profileExpectation.fulfill() + } + + func profileErr(_ error: NSError) { + XCTFail("Expected success, got error: \(error)") + profileExpectation.fulfill() + } + + func targetingOk(_ result: NSDictionary) { + XCTAssert(result.allKeys.isEmpty == false) + targetExpectation.fulfill() + } + + func targetingErr(_ error: NSError) { + XCTFail("Expected success, got error: \(error)") + targetExpectation.fulfill() + } + + func witnessOk(_ result: HTTPURLResponse) { + XCTAssert(result.allHeaderFields.keys.contains("x-optable-visitor")) + XCTAssert(result.statusCode == 200) + witnessExpectation.fulfill() + } + + func witnessErr(_ error: NSError) { + XCTFail("Expected success, got error: \(error)") + witnessExpectation.fulfill() + } +} diff --git a/Tests/OptableSDKTests.swift b/Tests/OptableSDKTests.swift deleted file mode 100644 index 711f0b7..0000000 --- a/Tests/OptableSDKTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// OptableSDKTests.swift -// OptableSDKTests -// -// Copyright © 2020 Optable Technologies Inc. All rights reserved. -// See LICENSE for details. -// - -@testable import OptableSDK -import XCTest - -class OptableSDKTests: XCTestCase { - var config: OptableConfig! - var sdk: OptableSDK! - - let defaultConfig = OptableConfig(tenant: "test-tenant", originSlug: "test-slug", insecure: false) - - override func setUpWithError() throws { - config = defaultConfig - sdk = OptableSDK(config: config) - } - - override func tearDownWithError() throws {} - - func test_identify() throws { - // TODO: impl - } - - func test_target() throws { - // TODO: impl - } - - func test_witness() throws { - // TODO: impl - } - - func test_profile() throws { - // TODO: impl - } -} diff --git a/Tests/EdgeAPITests.swift b/Tests/Unit/EdgeAPITests.swift similarity index 100% rename from Tests/EdgeAPITests.swift rename to Tests/Unit/EdgeAPITests.swift diff --git a/Tests/OptableIdentifierEncoderTests.swift b/Tests/Unit/OptableIdentifierEncoderTests.swift similarity index 99% rename from Tests/OptableIdentifierEncoderTests.swift rename to Tests/Unit/OptableIdentifierEncoderTests.swift index e3837b0..07e3c68 100644 --- a/Tests/OptableIdentifierEncoderTests.swift +++ b/Tests/Unit/OptableIdentifierEncoderTests.swift @@ -11,7 +11,7 @@ import XCTest class OptableIdentifierEncoderTests: XCTestCase { typealias SUT = OptableIdentifierEncoder - + func test_email() throws { var expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" XCTAssertEqual(expected, SUT.email("123")) diff --git a/Tests/OptableIdentifiersTests.swift b/Tests/Unit/OptableIdentifiersTests.swift similarity index 96% rename from Tests/OptableIdentifiersTests.swift rename to Tests/Unit/OptableIdentifiersTests.swift index f359825..4f6f75a 100644 --- a/Tests/OptableIdentifiersTests.swift +++ b/Tests/Unit/OptableIdentifiersTests.swift @@ -36,6 +36,7 @@ class OptableIdentifiersTests: XCTestCase { custom: [ "c": "d29c551097b9dd0b82423827f65161232efaf7fc", "c1": "AaaZza.dh012", + "c2": "", ] ) try test_json_generation_list(oids: oids) @@ -58,6 +59,7 @@ class OptableIdentifiersTests: XCTestCase { "utiq": "496f5db5-681f-4392-acd5-0d4f6e2f6b88", "c": "d29c551097b9dd0b82423827f65161232efaf7fc", "c1": "AaaZza.dh012", + "c2": "", ]) try test_json_generation_list(oids: oids) } @@ -79,6 +81,7 @@ class OptableIdentifiersTests: XCTestCase { .utiq: "496f5db5-681f-4392-acd5-0d4f6e2f6b88", .custom(nil): "d29c551097b9dd0b82423827f65161232efaf7fc", .custom(1): "AaaZza.dh012", + .custom(2): "", ]) try test_json_generation_list(oids: oids) } @@ -101,5 +104,7 @@ class OptableIdentifiersTests: XCTestCase { XCTAssertTrue(decodedData.contains(where: { $0 == "n:_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ" })) XCTAssertTrue(decodedData.contains(where: { $0 == "c:d29c551097b9dd0b82423827f65161232efaf7fc" })) XCTAssertTrue(decodedData.contains(where: { $0 == "c1:AaaZza.dh012" })) + // Empty should be ignored + XCTAssertFalse(decodedData.contains(where: { $0 == "c1:" })) } } From c880515068830947fe419ab9f34b41d7b2602dd3 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Thu, 18 Dec 2025 15:36:26 +0100 Subject: [PATCH 28/42] - Test fixes --- Tests/Unit/OptableIdentifiersTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Unit/OptableIdentifiersTests.swift b/Tests/Unit/OptableIdentifiersTests.swift index 4f6f75a..cf6ef2a 100644 --- a/Tests/Unit/OptableIdentifiersTests.swift +++ b/Tests/Unit/OptableIdentifiersTests.swift @@ -105,6 +105,6 @@ class OptableIdentifiersTests: XCTestCase { XCTAssertTrue(decodedData.contains(where: { $0 == "c:d29c551097b9dd0b82423827f65161232efaf7fc" })) XCTAssertTrue(decodedData.contains(where: { $0 == "c1:AaaZza.dh012" })) // Empty should be ignored - XCTAssertFalse(decodedData.contains(where: { $0 == "c1:" })) + XCTAssertFalse(decodedData.contains(where: { $0.contains("c2:") })) } } From aa82a61c1d9cea4b6ddb471a2a0c3126cf8f7325 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Thu, 18 Dec 2025 16:28:27 +0100 Subject: [PATCH 29/42] - Fixed SPM warning --- .../xcode/package.xcworkspace/contents.xcworkspacedata | 7 +++++++ Package.swift | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Package.swift b/Package.swift index da773f7..77fd81b 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,7 @@ let package = Package( name: "OptableSDK", dependencies: [], path: "Source", - exclude: ["Source/Info.plist"]), + exclude: ["Info.plist"]), .testTarget( name: "OptableSDKTests", dependencies: ["OptableSDK"], From 6217d7f0fdb307393c280bc3dfc1df65e89c30b7 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Fri, 19 Dec 2025 15:44:43 +0100 Subject: [PATCH 30/42] - Extended OptableConfig with Privacy Regulations params; Updated EdgeAPI --- OptableSDK.xcodeproj/project.pbxproj | 1 + Source/Core/EdgeAPI.swift | 45 ++++++++++++++++++++++--- Source/Core/IABConsent.swift | 37 +++++++++++++++++++++ Source/OptableConfig.swift | 49 ++++++++++++++++++++++++++++ Source/OptableIdentifiers.swift | 6 ++-- 5 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 Source/Core/IABConsent.swift diff --git a/OptableSDK.xcodeproj/project.pbxproj b/OptableSDK.xcodeproj/project.pbxproj index f5b1e62..1483d30 100644 --- a/OptableSDK.xcodeproj/project.pbxproj +++ b/OptableSDK.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ membershipExceptions = ( Integration/OptableSDKTests.swift, Misc/cartesianProduct.swift, + Misc/Constants.swift, Unit/EdgeAPITests.swift, Unit/OptableIdentifierEncoderTests.swift, Unit/OptableIdentifiersTests.swift, diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index 2baaf64..bb1bcf4 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -41,26 +41,26 @@ final class EdgeAPI { // MARK: Endpoints func identify(ids: OptableIdentifiers, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = buildEdgeAPIURL(endpoint:"identify") else { return nil } + guard let url = buildEdgeAPIURL(endpoint: "identify") else { return nil } let jsonData = try jsonEncoder.encode(ids) let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: jsonData) return dispatchRequest(req, completionHandler) } func profile(traits: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = buildEdgeAPIURL(endpoint:"profile") else { return nil } + guard let url = buildEdgeAPIURL(endpoint: "profile") else { return nil } let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), obj: ["traits": traits]) return dispatchRequest(req, completionHandler) } func targeting(completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = buildEdgeAPIURL(endpoint:"targeting") else { return nil } + guard let url = buildEdgeAPIURL(endpoint: "targeting") else { return nil } let req = try buildRequest(.GET, url: url, headers: resolveHeaders()) return dispatchRequest(req, completionHandler) } func witness(event: String, properties: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = buildEdgeAPIURL(endpoint:"witness") else { return nil } + guard let url = buildEdgeAPIURL(endpoint: "witness") else { return nil } let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), obj: ["event": event, "properties": properties]) return dispatchRequest(req, completionHandler) } @@ -126,7 +126,7 @@ extension EdgeAPI { } } - private func resolveHeaders() -> HTTPHeaders { + func resolveHeaders() -> HTTPHeaders { var headers = HTTPHeaders() headers[.accept] = "application/json" headers[.contentType] = "application/json" @@ -187,6 +187,41 @@ extension EdgeAPI { .init(name: "o", value: config.originSlug.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), .init(name: "osdk", value: OptableSDK.version.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), ] + + if let reg = config.reg { + components.queryItems?.append( + .init(name: "reg", value: reg.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)) + ) + } + + if let gdprConsent = config.gdprConsent, let gdpr = config.gdpr?.boolValue { + components.queryItems?.append(contentsOf: [ + .init(name: "gdpr_consent", value: gdprConsent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), + .init(name: "gdpr", value: "\(gdpr ? 1 : 0)"), + ]) + } else if let globalGDPRConsent = IABConsent.gdprTC, let globalGDPR = IABConsent.gdprApplies { + components.queryItems?.append(contentsOf: [ + .init(name: "gdpr_consent", value: globalGDPRConsent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), + .init(name: "gdpr", value: "\(globalGDPR ? 1 : 0)"), + ]) + } + + if let gpp = config.gpp { + components.queryItems?.append( + .init(name: "gpp", value: gpp.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)) + ) + } else if let globalGPP = IABConsent.gppTC { + components.queryItems?.append( + .init(name: "gpp", value: globalGPP.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)) + ) + } + + if let gppSid = config.gppSid { + components.queryItems?.append( + .init(name: "gpp_sid", value: gppSid.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)) + ) + } + return components.url } } diff --git a/Source/Core/IABConsent.swift b/Source/Core/IABConsent.swift new file mode 100644 index 0000000..8f1eb64 --- /dev/null +++ b/Source/Core/IABConsent.swift @@ -0,0 +1,37 @@ +// +// IABConsent.swift +// OptableSDK +// +// Created by user on 19.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +import Foundation + +/** + IABConsent is responsible retrieving user consent according to the IAB Transparency & Consent Framework + + For more info check: [](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md) + */ +enum IABConsent { + enum Keys { + static let IABTCF_TCString = "IABTCF_TCString" + static let IABTCF_gdprApplies = "IABTCF_gdprApplies" + static let IABGPP_2_TCString = "IABGPP_2_TCString" + } + + static var gdprApplies: Bool? { + if let iabValue = UserDefaults.standard.string(forKey: Keys.IABTCF_gdprApplies) { + return NSString(string: iabValue).boolValue + } + return nil + } + + static var gdprTC: String? { + UserDefaults.standard.string(forKey: Keys.IABTCF_TCString) + } + + static var gppTC: String? { + UserDefaults.standard.string(forKey: Keys.IABGPP_2_TCString) + } +} diff --git a/Source/OptableConfig.swift b/Source/OptableConfig.swift index 9acc5e2..a4ca881 100644 --- a/Source/OptableConfig.swift +++ b/Source/OptableConfig.swift @@ -10,6 +10,7 @@ import Foundation @objc public class OptableConfig: NSObject { + // MARK: Required /// The tenant name associated with the configuration. E.g. `acmeco.optable.co` => `acmeco`. @objc public var tenant: String @@ -18,6 +19,7 @@ public class OptableConfig: NSObject { @objc public var originSlug: String + // MARK: Optional /// The hostname of the Optable endpoint. Default value is "na.edge.optable.co". @objc public var host: String = "na.edge.optable.co" @@ -42,6 +44,53 @@ public class OptableConfig: NSObject { @objc public var skipAdvertisingIdDetection: Bool = false + // MARK: Privacy Regulations + /** + Optable privacy regulation override, which can be one of: gdpr, can, us, or null and will override all other privacy regulations when present. + */ + @objc + public var reg: String? + + /** + TCF EU v2 consent string. + + > If not set, SDK will try to fetch data from UserDefaults => `IABTCF_TCString`, as stated in [](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details) + */ + @objc + public var gdprConsent: String? + + /** + A boolean indicating whether GDPR applies, represented as a integer (0 when it does not apply, 1 when it does). This value should be present when gdpr_consent is supplied. + + > If not set, SDK will try to fetch data from UserDefaults => `IABTCF_gdprApplies`, as stated in [](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details) + */ + @objc + public var gdpr: NSNumber? = false + + /** + GPP privacy string. + + > If not set, SDK will try to fetch data from UserDefaults => `IABGPP_2_TCString`, as stated in [](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details) + */ + @objc + public var gpp: String? + + /** + A comma-separated list of up to two sections applicable in a given GPP privacy string. This value is required when gpp is present. + */ + @objc + public var gppSid: String? + + // TODO: timeout per-request? + /// Timeout for requests in the form of `{{timeout}}ms`. This timeout will override all other timeouts. +// @objc +// public var timeout: TimeInterval = 0 + + /// The Optable passport JWT. +// @objc +// public var passport: String? + + // MARK: Inits /** - Parameters: - tenant: The tenant name associated with the configuration. E.g. `acmeco.optable.co` => `acmeco`. diff --git a/Source/OptableIdentifiers.swift b/Source/OptableIdentifiers.swift index f1b347e..6304655 100644 --- a/Source/OptableIdentifiers.swift +++ b/Source/OptableIdentifiers.swift @@ -60,16 +60,16 @@ public struct OptableIdentifiers { set { dict[key] = newValue } } - init(_ dict: [OptableIdentifierType: String]) { + public init(_ dict: [OptableIdentifierType: String]) { self.dict = Dictionary(uniqueKeysWithValues: dict.map({ ($0.key.rawValue, $0.value) })) } - subscript(_ key: OptableIdentifierType) -> String? { + public subscript(_ key: OptableIdentifierType) -> String? { get { dict[key.rawValue] } set { dict[key.rawValue] = newValue } } - func generateEnrichedIds() -> [String] { + public func generateEnrichedIds() -> [String] { var results: [String] = [] for (key, value) in dict { From 544065709c80eb2ce09489f686c7538fd7376956 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Fri, 19 Dec 2025 15:45:05 +0100 Subject: [PATCH 31/42] - Updated Tests --- Tests/Integration/OptableSDKTests.swift | 2 +- Tests/Misc/Constants.swift | 55 ++++++++++ Tests/Unit/EdgeAPITests.swift | 128 ++++++++++++++++++++---- 3 files changed, 163 insertions(+), 22 deletions(-) create mode 100644 Tests/Misc/Constants.swift diff --git a/Tests/Integration/OptableSDKTests.swift b/Tests/Integration/OptableSDKTests.swift index 3bfc90d..1c8e065 100644 --- a/Tests/Integration/OptableSDKTests.swift +++ b/Tests/Integration/OptableSDKTests.swift @@ -11,7 +11,7 @@ import XCTest // MARK: - OptableSDKTests class OptableSDKTests: XCTestCase { - let defaultConfig = OptableConfig(tenant: "prebidtest", originSlug: "ios-sdk", insecure: false, customUserAgent: "ios-integration-tests") + let defaultConfig = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK, insecure: false, customUserAgent: T.api.userAgent) lazy var sdk = OptableSDK(config: defaultConfig) lazy var identifyExpectation = expectation(description: "identify-delegate-expectation") diff --git a/Tests/Misc/Constants.swift b/Tests/Misc/Constants.swift new file mode 100644 index 0000000..5e6fd4c --- /dev/null +++ b/Tests/Misc/Constants.swift @@ -0,0 +1,55 @@ +// +// Constants.swift +// OptableSDK +// +// Created by user on 19.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +import Foundation + +enum T { + enum api { + enum host { + static let na: String = "na.edge.optable.co" + static let au: String = "au.edge.optable.co" + + static let all: [String] = [na, au] + } + + enum endpoint { + static let identify: String = "identify" + static let target: String = "target" + static let witness: String = "witness" + static let profile: String = "profile" + + static let all: [String] = [identify, target, witness, profile] + } + + enum path { + static let v1: String = "v1" + static let v2: String = "v2" + + static let all: [String] = [v1, v2] + } + + enum tenant { + static let prebidtest: String = "prebidtest" + static let test: String = "test-tenant" + + static let all: [String] = [prebidtest, test] + } + + enum slug { + static let iosSDK: String = "ios-sdk" + static let jsSDK: String = "js-sdk" + + static let all: [String] = [iosSDK, jsSDK] + } + + static let userAgent: String = "ios-integration-tests" + + static let apiKey: String = "test-api-key" + static let apiKeyBearer: String = "Bearer \(apiKey)" + } +} diff --git a/Tests/Unit/EdgeAPITests.swift b/Tests/Unit/EdgeAPITests.swift index e18b602..e99d9bc 100644 --- a/Tests/Unit/EdgeAPITests.swift +++ b/Tests/Unit/EdgeAPITests.swift @@ -10,41 +10,39 @@ import XCTest class EdgeAPITests: XCTestCase { - typealias TestCaseConfiguration = (insecure: Bool, host: String, path: String, endpoint: String, tenant: String, slug: String) - var defaultTestConfiguration: TestCaseConfiguration { - (insecure: false, host: "na.edge.optable.co", path: "v2", endpoint: "", tenant: "test-tenant", slug: "test-slug") - } - /** Expected output: `https://{{Domain}}/{{API_ENDPOINT}}?t={{TENANT}}&o={{SOURCE_SLUG}}` - + For more info check: [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide) */ - func test_edge_api_url_generation() throws { - - let hosts = ["na.edge.optable.co", "au.edge.optable.co"] - let endpoints = ["identify", "profile"] - let paths = ["v1", "v2"] - let tenants = ["prebidtest", "test-tenant"] - let slugs = ["ios-sdk", "js-sdk"] - + func test_url_generation() throws { + let hosts = T.api.host.all + let endpoints = T.api.endpoint.all + let paths = T.api.path.all + let tenants = T.api.tenant.all + let slugs = T.api.slug.all + + typealias TestCaseConfiguration = (insecure: Bool, host: String, path: String, endpoint: String, tenant: String, slug: String) + cartesianProduct([hosts, paths, endpoints, tenants, slugs]) .map({ product in - var testConfig = defaultTestConfiguration - testConfig.host = product[0] - testConfig.path = product[1] - testConfig.endpoint = product[2] - testConfig.tenant = product[3] - testConfig.slug = product[4] + let testConfig: TestCaseConfiguration = ( + insecure: false, + host: product[0], + path: product[1], + endpoint: product[2], + tenant: product[3], + slug: product[4] + ) return testConfig }) .forEach({ (testConfig: TestCaseConfiguration) in let edgeAPI = EdgeAPI(OptableConfig(tenant: testConfig.tenant, originSlug: testConfig.slug, host: testConfig.host, path: testConfig.path, insecure: testConfig.insecure)) let generatedURL = edgeAPI.buildEdgeAPIURL(endpoint: testConfig.endpoint) let generatedURLComponents = URLComponents(url: generatedURL!, resolvingAgainstBaseURL: false)! - + XCTAssertEqual(generatedURLComponents.scheme, testConfig.insecure ? "http" : "https") XCTAssertEqual(generatedURLComponents.host, testConfig.host) XCTAssertEqual(generatedURLComponents.path, "/\(testConfig.path)/\(testConfig.endpoint)") @@ -54,4 +52,92 @@ class EdgeAPITests: XCTestCase { XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "o" })!.value, testConfig.slug) }) } + + /** + For more info check: [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide#parameters) + */ + func test_url_generation_privacy_regulations_empty() throws { + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABTCF_gdprApplies) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABTCF_TCString) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_2_TCString) + + let config = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK) + let generatedURL = OptableSDK(config: config).api.buildEdgeAPIURL(endpoint: T.api.endpoint.identify) + let generatedURLComponents = URLComponents(url: generatedURL!, resolvingAgainstBaseURL: false)! + + XCTAssertNil(generatedURLComponents.queryItems?.first(where: { $0.name == "reg" })) + XCTAssertNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gdpr_consent" })) + XCTAssertNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gdpr" })) + XCTAssertNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gpp" })) + XCTAssertNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gpp_sid" })) + } + + /** + For more info check: [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide#parameters) + */ + func test_url_generation_privacy_regulations_global() throws { + UserDefaults.standard.set("0", forKey: IABConsent.Keys.IABTCF_gdprApplies) + UserDefaults.standard.set("globalGDPRConsent", forKey: IABConsent.Keys.IABTCF_TCString) + UserDefaults.standard.set("globalGPP", forKey: IABConsent.Keys.IABGPP_2_TCString) + + let config = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK) + let generatedURL = OptableSDK(config: config).api.buildEdgeAPIURL(endpoint: T.api.endpoint.identify) + let generatedURLComponents = URLComponents(url: generatedURL!, resolvingAgainstBaseURL: false)! + + XCTAssertNil(generatedURLComponents.queryItems?.first(where: { $0.name == "reg" })) + XCTAssertNotNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gdpr_consent" })) + XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "gdpr_consent" })!.value, "globalGDPRConsent") + XCTAssertNotNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gdpr" })) + XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "gdpr" })!.value, "0") + XCTAssertNotNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gpp" })) + XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "gpp" })!.value, "globalGPP") + XCTAssertNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gpp_sid" })) + } + + /** + For more info check: [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide#parameters) + */ + func test_url_generation_privacy_regulations_explicit() throws { + UserDefaults.standard.set("0", forKey: IABConsent.Keys.IABTCF_gdprApplies) + UserDefaults.standard.set("globalGDPRConsent", forKey: IABConsent.Keys.IABTCF_TCString) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_2_TCString) + + let config = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK) + config.reg = "reg" + config.gdprConsent = "gdprConsent" + config.gdpr = 1 + config.gpp = "gpp" + config.gppSid = "gppSid" + + let generatedURL = OptableSDK(config: config).api.buildEdgeAPIURL(endpoint: T.api.endpoint.identify) + let generatedURLComponents = URLComponents(url: generatedURL!, resolvingAgainstBaseURL: false)! + + XCTAssertNotNil(generatedURLComponents.queryItems?.first(where: { $0.name == "reg" })) + XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "reg" })!.value, "reg") + XCTAssertNotNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gdpr_consent" })) + XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "gdpr_consent" })!.value, "gdprConsent") + XCTAssertNotNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gdpr" })) + XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "gdpr" })!.value, "1") + XCTAssertNotNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gpp" })) + XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "gpp" })!.value, "gpp") + XCTAssertNotNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gpp_sid" })) + XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "gpp_sid" })!.value, "gppSid") + } + + /** + For more info check: [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide#parameters) + */ + func test_header_generation() throws { + let config = OptableConfig( + tenant: T.api.tenant.prebidtest, + originSlug: T.api.slug.iosSDK, + apiKey: T.api.apiKey, + customUserAgent: T.api.userAgent, + ) + let sdk = OptableSDK(config: config) + let generatedHeaders = sdk.api.resolveHeaders().asDict + + XCTAssertEqual(generatedHeaders["User-Agent"], T.api.userAgent) + XCTAssertEqual(generatedHeaders["Authorization"], T.api.apiKeyBearer) + } } From 5dde325015320b745670236dc87c2a7fa2e344eb Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Fri, 19 Dec 2025 15:50:39 +0100 Subject: [PATCH 32/42] - Updated project structure --- Source/{Core => Misc}/AppTrackingTransparency.swift | 0 Source/{Core => Misc}/IABConsent.swift | 0 Source/{Core => Misc}/Networking.swift | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename Source/{Core => Misc}/AppTrackingTransparency.swift (100%) rename Source/{Core => Misc}/IABConsent.swift (100%) rename Source/{Core => Misc}/Networking.swift (100%) diff --git a/Source/Core/AppTrackingTransparency.swift b/Source/Misc/AppTrackingTransparency.swift similarity index 100% rename from Source/Core/AppTrackingTransparency.swift rename to Source/Misc/AppTrackingTransparency.swift diff --git a/Source/Core/IABConsent.swift b/Source/Misc/IABConsent.swift similarity index 100% rename from Source/Core/IABConsent.swift rename to Source/Misc/IABConsent.swift diff --git a/Source/Core/Networking.swift b/Source/Misc/Networking.swift similarity index 100% rename from Source/Core/Networking.swift rename to Source/Misc/Networking.swift From ab2b02536bb60b4af64a35700fc271d5a30ef62d Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Fri, 19 Dec 2025 17:48:43 +0100 Subject: [PATCH 33/42] - Updated `targeting` and `profile` endpoints to match API; Made EdgeAPI more test-friendly --- .../xcschemes/OptableSDK.xcscheme | 3 +- Source/Core/EdgeAPI.swift | 52 +++++--- Source/Misc/URL+Compat.swift | 22 ++++ Source/OptableSDK.swift | 62 ++++++---- Tests/Unit/EdgeAPITests.swift | 114 ++++++++++++++++-- 5 files changed, 206 insertions(+), 47 deletions(-) create mode 100644 Source/Misc/URL+Compat.swift diff --git a/OptableSDK.xcodeproj/xcshareddata/xcschemes/OptableSDK.xcscheme b/OptableSDK.xcodeproj/xcshareddata/xcschemes/OptableSDK.xcscheme index 7a915cb..5a0ef77 100644 --- a/OptableSDK.xcodeproj/xcshareddata/xcschemes/OptableSDK.xcscheme +++ b/OptableSDK.xcodeproj/xcshareddata/xcschemes/OptableSDK.xcscheme @@ -26,7 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index bb1bcf4..172dc05 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -40,36 +40,53 @@ final class EdgeAPI { } // MARK: Endpoints - func identify(ids: OptableIdentifiers, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + func identify(ids: OptableIdentifiers) throws -> URLRequest? { guard let url = buildEdgeAPIURL(endpoint: "identify") else { return nil } let jsonData = try jsonEncoder.encode(ids) - let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: jsonData) - return dispatchRequest(req, completionHandler) + let request = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: jsonData) + return request } - func profile(traits: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + func profile(traits: NSDictionary, id: String? = nil, neighbors: [String]? = nil) throws -> URLRequest? { guard let url = buildEdgeAPIURL(endpoint: "profile") else { return nil } - let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), obj: ["traits": traits]) - return dispatchRequest(req, completionHandler) + + var payload: [String: Any] = ["traits": traits] + + if let id { + payload["id"] = id + } + + if let neighbors, neighbors.isEmpty == false { + payload["neighbors"] = neighbors + } + + let request = try buildRequest(.POST, url: url, headers: resolveHeaders(), obj: payload) + return request } - func targeting(completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { - guard let url = buildEdgeAPIURL(endpoint: "targeting") else { return nil } - let req = try buildRequest(.GET, url: url, headers: resolveHeaders()) - return dispatchRequest(req, completionHandler) + func targeting(ids: [String]? = nil) throws -> URLRequest? { + guard var url = buildEdgeAPIURL(endpoint: "targeting") else { return nil } + + if let ids { + let queryItems = ids.compactMap({ URLQueryItem(name: "id", value: $0) }) + url.compatAppend(queryItems: queryItems) + } + + let request = try buildRequest(.GET, url: url, headers: resolveHeaders()) + return request } - func witness(event: String, properties: NSDictionary, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) throws -> URLSessionDataTask? { + func witness(event: String, properties: NSDictionary) throws -> URLRequest? { guard let url = buildEdgeAPIURL(endpoint: "witness") else { return nil } - let req = try buildRequest(.POST, url: url, headers: resolveHeaders(), obj: ["event": event, "properties": properties]) - return dispatchRequest(req, completionHandler) + let request = try buildRequest(.POST, url: url, headers: resolveHeaders(), obj: ["event": event, "properties": properties]) + return request } } -// MARK: - Private +// MARK: - Dispatch extension EdgeAPI { - private func dispatchRequest(_ req: URLRequest, _ completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { - return URLSession.shared.dataTask(with: req) { data, response, error in + func dispatch(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { + return URLSession.shared.dataTask(with: request) { data, response, error in guard let res = response as? HTTPURLResponse, error == nil else { completionHandler(data, response, error) return @@ -101,7 +118,10 @@ extension EdgeAPI { completionHandler(data, response, error) } } +} +// MARK: - Private +extension EdgeAPI { private func resolveUserAgent(callback: @escaping (_ useragent: String) -> Void) { var wkUserAgent = "" let myGroup = DispatchGroup() diff --git a/Source/Misc/URL+Compat.swift b/Source/Misc/URL+Compat.swift new file mode 100644 index 0000000..1f323b0 --- /dev/null +++ b/Source/Misc/URL+Compat.swift @@ -0,0 +1,22 @@ +// +// URL+Compat.swift +// OptableSDK +// +// Created by user on 19.12.2025. +// Copyright © 2025 Optable Technologies, Inc. All rights reserved. +// + +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 + } + } +} diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index 3b416aa..575ede4 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -134,8 +134,8 @@ 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(). */ - func targeting(completion: @escaping (Result) -> Void) throws { - try _targeting(completion: completion) + func targeting(ids: [String]? = nil, completion: @escaping (Result) -> Void) throws { + try _targeting(ids: ids, completion: completion) } /// targetingFromCache() returns the previously cached targeting data, if any. @@ -160,10 +160,10 @@ public extension OptableSDK { Instead of completion callbacks, function have to be awaited. */ @available(iOS 13.0, *) - func targeting() async throws -> NSDictionary { + func targeting(ids: [String]? = nil) async throws -> NSDictionary { return try await withCheckedThrowingContinuation({ [unowned self] continuation in do { - try self._targeting(completion: { continuation.resume(with: $0) }) + try self._targeting(ids: ids, completion: { continuation.resume(with: $0) }) } catch { continuation.resume(throwing: error) } @@ -177,15 +177,15 @@ public extension OptableSDK { Instead of completion callbacks, delegate methods are called. */ @objc - func targeting() throws { - try self._targeting { result in + func targeting(ids: [String]? = nil) throws { + try self._targeting(ids: ids, completion: { result in switch result { case let .success(keyvalues): self.delegate?.targetingOk(keyvalues) case let .failure(error as NSError): self.delegate?.targetingErr(error) } - } + }) } } @@ -245,8 +245,8 @@ 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, _ completion: @escaping (Result) -> Void) throws { - try _profile(traits: traits, completion: completion) + 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) } // MARK: Async/Await support @@ -256,10 +256,10 @@ public extension OptableSDK { Instead of completion callbacks, function have to be awaited. */ @available(iOS 13.0, *) - func profile(traits: NSDictionary) async throws -> HTTPURLResponse { + func profile(traits: NSDictionary, id: String? = nil, neighbors: [String]? = nil) async throws -> HTTPURLResponse { return try await withCheckedThrowingContinuation({ [unowned self] continuation in do { - try self._profile(traits: traits, completion: { continuation.resume(with: $0) }) + try self._profile(traits: traits, id: id, neighbors: neighbors, completion: { continuation.resume(with: $0) }) } catch { continuation.resume(throwing: error) } @@ -273,8 +273,8 @@ public extension OptableSDK { Instead of completion callbacks, delegate methods are called. */ @objc - func profile(traits: NSDictionary) throws { - try _profile(traits: traits, completion: { result in + func profile(traits: NSDictionary, id: String? = nil, neighbors: [String]? = nil) throws { + try _profile(traits: traits, id: id, neighbors: neighbors, completion: { result in switch result { case let .success(response): self.delegate?.profileOk(response) @@ -296,7 +296,11 @@ private extension OptableSDK { ids[.appleIDFA] = ATT.advertisingIdentifier.uuidString } - try api.identify(ids: ids, completionHandler: { data, response, error in + guard let request = try api.identify(ids: ids) else { + throw OptableError.identify("Failed to create identify request") + } + + api.dispatch(request: request, completionHandler: { data, response, error in guard let response = response as? HTTPURLResponse, error == nil, data != nil else { if let err = error { completion(.failure(OptableError.identify("Session error: \(err)"))) @@ -311,11 +315,15 @@ private extension OptableSDK { return } completion(.success(response)) - })?.resume() + }).resume() } - private func _targeting(completion: @escaping (Result) -> Void) throws { - try api.targeting(completionHandler: { data, response, error in + private func _targeting(ids: [String]?, completion: @escaping (Result) -> Void) throws { + guard let request = try api.targeting(ids: ids) else { + throw OptableError.targeting("Failed to create targeting request") + } + + api.dispatch(request: request, completionHandler: { data, response, error in guard let response = response as? HTTPURLResponse, error == nil, data != nil else { if let err = error { completion(.failure(OptableError.targeting("Session error: \(err)"))) @@ -341,11 +349,15 @@ private extension OptableSDK { } catch { completion(.failure(OptableError.targeting("Error parsing JSON response: \(error)"))) } - })?.resume() + }).resume() } func _witness(event: String, properties: NSDictionary, completion: @escaping (Result) -> Void) throws { - try api.witness(event: event, properties: properties, completionHandler: { data, response, error in + guard let request = try api.witness(event: event, properties: properties) else { + throw OptableError.witness("Failed to create witness request") + } + + api.dispatch(request: request, completionHandler: { data, response, error in guard let response = response as? HTTPURLResponse, error == nil else { if let err = error { completion(.failure(OptableError.witness("Session error: \(err)"))) @@ -360,11 +372,15 @@ private extension OptableSDK { return } completion(.success(response)) - })?.resume() + }).resume() } - func _profile(traits: NSDictionary, completion: @escaping (Result) -> Void) throws { - try api.profile(traits: traits, completionHandler: { data, response, error in + func _profile(traits: NSDictionary, id: String?, neighbors: [String]?, completion: @escaping (Result) -> Void) throws { + guard let request = try api.profile(traits: traits, id: id, neighbors: neighbors) else { + throw OptableError.profile("Failed to create profile request") + } + + api.dispatch(request: request, completionHandler: { data, response, error in guard let response = response as? HTTPURLResponse, error == nil else { if let err = error { completion(.failure(OptableError.profile("Session error: \(err)"))) @@ -379,7 +395,7 @@ private extension OptableSDK { return } completion(.success(response)) - })?.resume() + }).resume() } private static func generateEdgeAPIErrorDescription(with data: Data?, response: HTTPURLResponse) -> String { diff --git a/Tests/Unit/EdgeAPITests.swift b/Tests/Unit/EdgeAPITests.swift index e99d9bc..fa5b41f 100644 --- a/Tests/Unit/EdgeAPITests.swift +++ b/Tests/Unit/EdgeAPITests.swift @@ -10,6 +10,15 @@ import XCTest class EdgeAPITests: XCTestCase { + lazy var config = OptableConfig( + tenant: T.api.tenant.prebidtest, + originSlug: T.api.slug.iosSDK, + apiKey: T.api.apiKey, + customUserAgent: T.api.userAgent, + ) + lazy var sdk = OptableSDK(config: config) + + // MARK: URL-s /** Expected output: `https://{{Domain}}/{{API_ENDPOINT}}?t={{TENANT}}&o={{SOURCE_SLUG}}` @@ -124,20 +133,111 @@ class EdgeAPITests: XCTestCase { XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "gpp_sid" })!.value, "gppSid") } + // MARK: Header-s /** For more info check: [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide#parameters) */ func test_header_generation() throws { - let config = OptableConfig( - tenant: T.api.tenant.prebidtest, - originSlug: T.api.slug.iosSDK, - apiKey: T.api.apiKey, - customUserAgent: T.api.userAgent, - ) - let sdk = OptableSDK(config: config) let generatedHeaders = sdk.api.resolveHeaders().asDict XCTAssertEqual(generatedHeaders["User-Agent"], T.api.userAgent) XCTAssertEqual(generatedHeaders["Authorization"], T.api.apiKeyBearer) } + + // MARK: URLRequest-s + /** + For more info check: [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide/optable-real-time-api-endpoints) + */ + func test_identify_request_generation() throws { + let urlRequest = try sdk.api.identify(ids: OptableIdentifiers(postalCode: "1234567890")) + + // Method + XCTAssertEqual(urlRequest?.httpMethod, HTTPMethod.POST.rawValue) + + // Path + let urlComponents = URLComponents(url: urlRequest!.url!, resolvingAgainstBaseURL: false)! + XCTAssert(urlComponents.path.contains("identify")) + + // Body + if let body = urlRequest?.httpBody { + if let jsonObj = try JSONSerialization.jsonObject(with: body) as? [String] { + XCTAssertEqual(jsonObj[0], "z:1234567890") + } else { + XCTFail("Not a valid JSON object") + } + } else { + XCTFail("No body") + } + } + + /** + 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: ["e:12345", "p:54321"]) + + // 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) + } + + /** + For more info check: [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide/optable-real-time-api-endpoints/profile) + */ + func test_profile_request_generation() throws { + let urlRequest = try sdk.api.profile(traits: ["test-key": "test-value"], id: "c:id2", neighbors: ["c:id1", "c:id3"]) + + // Method + XCTAssertEqual(urlRequest?.httpMethod, HTTPMethod.POST.rawValue) + + // Path + let urlComponents = URLComponents(url: urlRequest!.url!, resolvingAgainstBaseURL: false)! + XCTAssert(urlComponents.path.contains("profile")) + + // Body + if let body = urlRequest?.httpBody { + if let jsonObj = try JSONSerialization.jsonObject(with: body) as? NSDictionary { + XCTAssertEqual(jsonObj["id"] as! String, "c:id2") + XCTAssertEqual(jsonObj["neighbors"] as! [String], ["c:id1", "c:id3"]) + XCTAssertEqual(jsonObj["traits"] as! NSDictionary, ["test-key": "test-value"]) + } else { + XCTFail("Not a valid JSON object") + } + } else { + XCTFail("No body") + } + } + + /** + For more info check: [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide/optable-real-time-api-endpoints) + */ + func test_witness_request_generation() throws { + let urlRequest = try sdk.api.witness(event: "test-event", properties: ["test-key": "test-value"]) + + // Method + XCTAssertEqual(urlRequest?.httpMethod, HTTPMethod.POST.rawValue) + + // Path + let urlComponents = URLComponents(url: urlRequest!.url!, resolvingAgainstBaseURL: false)! + XCTAssert(urlComponents.path.contains("witness")) + + // Body + if let body = urlRequest?.httpBody { + if let jsonObj = try JSONSerialization.jsonObject(with: body) as? NSDictionary { + XCTAssertEqual(jsonObj["event"] as! String, "test-event") + XCTAssertEqual(jsonObj["properties"] as! NSDictionary, ["test-key": "test-value"]) + } else { + XCTFail("Not a valid JSON object") + } + } else { + XCTFail("No body") + } + } } From 111524b8eb3278c757b9441afb32b4ef4e1e50a3 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 22 Dec 2025 15:45:21 +0100 Subject: [PATCH 34/42] - Added auto ATT tracking auth request on SDK init --- Source/OptableSDK.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index 575ede4..c2e2101 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -56,6 +56,13 @@ public class OptableSDK: NSObject { public init(config: OptableConfig) { self.config = config self.api = EdgeAPI(config) + + // Automatically request Tracking Authorization + if #available(iOS 14, *) { + if config.skipAdvertisingIdDetection == false, ATT.canAuthorize { + ATT.requestATTAuthorization() + } + } } /// OptableSDK version @@ -292,7 +299,8 @@ private extension OptableSDK { 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)) { + ATT.advertisingIdentifier != UUID(uuid: uuid_t(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)), + ids[.appleIDFA] != nil { ids[.appleIDFA] = ATT.advertisingIdentifier.uuidString } From 08b6cd88f8bc2f36e8ecaf14f627d387995fef23 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 22 Dec 2025 15:45:45 +0100 Subject: [PATCH 35/42] - Updated demo-ios-objc --- .../demo-ios-objc/GAMBannerViewController.m | 20 ++++++++++++++----- .../demo-ios-objc/IdentifyViewController.m | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m b/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m index fd27a96..359369b 100644 --- a/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m +++ b/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m @@ -38,9 +38,14 @@ - (IBAction)loadBannerWithTargeting:(id)sender { [_targetingOutput setText:@"Calling /targeting API...\n"]; - [OPTABLE targetingAndReturnError:&error]; - [OPTABLE witness:@"GAMBannerViewController.loadBannerClicked" properties:@{ @"example": @"value" } error:&error]; - [OPTABLE profileWithTraits:@{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES } error:&error]; + [OPTABLE targetingWithIds: NULL error: &error]; + [OPTABLE witnessWithEvent: @"GAMBannerViewController.loadBannerClicked" + properties: @{ @"example": @"value" } + error: &error]; + [OPTABLE profileWithTraits: @{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES } + id: NULL + neighbors: NULL + error: &error]; } - (IBAction)loadBannerWithTargetingFromCache:(id)sender { @@ -62,8 +67,13 @@ - (IBAction)loadBannerWithTargetingFromCache:(id)sender { } [self.bannerView loadRequest:request]; - [OPTABLE witness:@"GAMBannerViewController.loadBannerClicked" properties:@{ @"example": @"value" } error:&error]; - [OPTABLE profileWithTraits:@{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES } error:&error]; + [OPTABLE witnessWithEvent: @"GAMBannerViewController.loadBannerClicked" + properties: @{ @"example": @"value" } + error: &error]; + [OPTABLE profileWithTraits: @{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES } + id: NULL + neighbors: NULL + error: &error]; } - (IBAction)clearTargetingCache:(id)sender { diff --git a/demo-ios-objc/demo-ios-objc/IdentifyViewController.m b/demo-ios-objc/demo-ios-objc/IdentifyViewController.m index 37ec37e..187eac9 100644 --- a/demo-ios-objc/demo-ios-objc/IdentifyViewController.m +++ b/demo-ios-objc/demo-ios-objc/IdentifyViewController.m @@ -36,7 +36,7 @@ - (IBAction)dispatchIdentify:(id)sender { NSError *error = nil; NSDictionary *ids = @{ @"e" : email, @"c" : @"new-custom.ABC" }; - [OPTABLE identify: ids error:&error]; + [OPTABLE identify: ids error: &error]; } @end From 2350c3bcde6b23fa3cc49ccd0c71c0ac24bc7689 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 22 Dec 2025 16:12:36 +0100 Subject: [PATCH 36/42] - Restored OptableSDK.eidFromURL(_:) --- Source/OptableSDK.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index c2e2101..4dd1474 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -292,6 +292,13 @@ public extension OptableSDK { } } +// MARK: - Identify from URL +public extension OptableSDK { + func eidFromURL(_ urlString: String) -> String { + OptableIdentifierEncoder.eidFromURL(urlString) + } +} + // MARK: - Private private extension OptableSDK { private func _identify(_ ids: OptableIdentifiers, completion: @escaping (Result) -> Void) throws { From 17c81e22f1fd58a77f2a64a2f560b0e4f3bc3dca Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 22 Dec 2025 16:16:02 +0100 Subject: [PATCH 37/42] - Updated docs --- README.md | 522 +++--------------------------- docs/identify-from-url.md | 0 releasing.md => docs/releasing.md | 0 docs/usage-objc.md | 187 +++++++++++ docs/usage-swift.md | 228 +++++++++++++ 5 files changed, 452 insertions(+), 485 deletions(-) create mode 100644 docs/identify-from-url.md rename releasing.md => docs/releasing.md (100%) create mode 100644 docs/usage-objc.md create mode 100644 docs/usage-swift.md diff --git a/README.md b/README.md index dccfcb2..30417dd 100644 --- a/README.md +++ b/README.md @@ -1,524 +1,76 @@ # Optable iOS SDK [![iOS SDK CI](https://github.com/Optable/optable-ios-sdk/actions/workflows/ios-sdk-ci.yml/badge.svg)](https://github.com/Optable/optable-ios-sdk/actions/workflows/ios-sdk-ci.yml) -Swift SDK for integrating with an [Optable Data Connectivity Node (DCN)](https://docs.optable.co) from an iOS application. +SDK for integrating with an [Optable Data Connectivity Node (DCN)](https://docs.optable.co) from an iOS application. -You can use the SDK functionality from either a Swift or Objective-C iOS application. +## Install -## Contents +### Swift Package Manager (SPM) -- [Optable iOS SDK ](#optable-ios-sdk-) - - [Contents](#contents) - - [Installing](#installing) - - [Swift Package Manager](#swift-package-manager) - - [CocoaPods](#cocoapods) - - [Using (Swift)](#using-swift) - - [Identify API](#identify-api) - - [Profile API](#profile-api) - - [Targeting API](#targeting-api) - - [Caching Targeting Data](#caching-targeting-data) - - [Witness API](#witness-api) - - [Integrating GAM360](#integrating-gam360) - - [Using (Objective-C)](#using-objective-c) - - [Identify API](#identify-api-1) - - [Profile API](#profile-api-1) - - [Targeting API](#targeting-api-1) - - [Caching Targeting Data](#caching-targeting-data-1) - - [Witness API](#witness-api-1) - - [Integrating GAM360](#integrating-gam360-1) - - [Identifying visitors arriving from Email newsletters](#identifying-visitors-arriving-from-email-newsletters) - - [Insert oeid into your Email newsletter template](#insert-oeid-into-your-email-newsletter-template) - - [Capture clicks on universal links in your application](#capture-clicks-on-universal-links-in-your-application) - - [Call tryIdentifyFromURL SDK API](#call-tryidentifyfromurl-sdk-api) - - [Swift](#swift) - - [Objective-C](#objective-c) - - [Demo Applications](#demo-applications) - - [Building](#building) +The [Swift Package Manager](https://www.swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the swift compiler. -## Installing - -The SDK can be installed using either the [Swift Package Manager](https://www.swift.org/package-manager/) or the [CocoaPods](https://cocoapods.org) dependency manager. - -### Swift Package Manager - -You can add this SDK _Package_ to your project. The manifest file is [Package.swift](https://github.com/Optable/optable-ios-sdk/blob/master/Package.swift) - -### CocoaPods - -This SDK can be installed via the [CocoaPods](https://cocoapods.org/) dependency manager. To install the latest [release](https://github.com/Optable/optable-ios-sdk/releases), you need to source the [public cocoapods](https://cdn.cocoapods.org/) repository as well as the `OptableSDK` pod from your `Podfile`: - -```ruby -platform :ios, '13.0' - -source 'https://cdn.cocoapods.org/' -... - -target 'YourProject' do - use_frameworks! - - pod 'OptableSDK' - ... -end -``` - -You can then run `pod install` to download all of your dependencies and prepare your project `xcworkspace`. - -If you would like to reference a specific [release](https://github.com/Optable/optable-ios-sdk/releases), simply append it to the referenced pod. For example: - -```ruby -pod 'OptableSDK', '0.8.2' -``` - -## Using (Swift) - -To configure an instance of the SDK integrating with an [Optable](https://optable.co/) DCN running at hostname `dcn.customer.com`, from a configured Swift application origin identified by slug `my-app`, you simply create an instance of the `OptableSDK` class through which you can communicate to the DCN. For example, from your `AppDelegate`: - -```swift -import OptableSDK -import UIKit -... - -var OPTABLE: OptableSDK? - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: - [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - ... - OPTABLE = OptableSDK(host: "dcn.customer.com", app: "my-app") - ... - return true - } - ... -} -``` - -Note that while the `OPTABLE` variable is global, we initialize it with an instance of `OptableSDK` in the `application()` method which runs at app launch, and not at the time it is declared. This is done because Swift's lazy-loading otherwise delays initialization to the first use of the variable. Both approaches work, though forcing early initialization allows the SDK to configure itself early. In particular, as part of its internal configuration the SDK will attempt to read the User-Agent string exposed by WebView and, since this is an asynchronous operation, it is best done as early as possible in the application lifecycle. - -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 all SDK communication with Optable DCNs is done over TLS. The only exception to this is if you instantiate the `OptableSDK` class with a third optional boolean parameter, `insecure`, set to `true`. For example: - -```swift -OPTABLE = OptableSDK(host: "dcn.customer.com", app: "my-app", insecure: true) -``` - -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 fourth string parameter, `useragent`, which allows you to set whatever user agent string you would like to send instead. For example: - -```swift -OPTABLE = OptableSDK(host: "dcn.customer.com", app: "my-app", insecure: false, useragent: "custom-ua") -``` - -The default value of `nil` for the `useragent` parameter enables the `WKWebView` auto-detection behavior. - -### Identify API - -To associate a user device with an authenticated identifier such as an Email address, or with other known IDs such as the Apple ID for Advertising (IDFA), or even your own vendor or app level `PPID`, you can call the `identify` API as follows: +Once you have your Swift package set up, you can add this SDK as a dependency. Add it to the dependencies value of your Package.swift or the Package list in Xcode. ```swift -let emailString = "some.email@address.com" -let sendIDFA = true - -do { - try OPTABLE!.identify(email: emailString, aaid: sendIDFA) { result in - switch (result) { - case .success(let response): - // identify API success, response.statusCode is HTTP response status 200 - case .failure(let error): - // handle identify API failure in `error` - } - } -} catch { - // handle thrown exception in `error` -} +dependencies: [ + .package(url: "https://github.com/Optable/optable-ios-sdk", .branch("master")) +] ``` -The SDK `identify()` method will asynchronously connect to the configured DCN and send IDs for resolution. The provided callback can be used to understand successful completion or errors. - -> :warning: **Client-Side Email Hashing**: The SDK will compute the SHA-256 hash of the Email address on the client-side and send the hashed value to the DCN. The Email address is **not** sent by the device in plain text. - -Since the `sendIDFA` value provided to `identify()` via the `aaid` (Apple Advertising ID or IDFA) boolean parameter is `true`, the SDK will attempt to fetch and send the Apple IDFA in the call to `identify` too, unless the user has turned on "Limit ad tracking" in their iOS device privacy settings. - -> :warning: **As of iOS 14.0**, Apple has introduced [additional restrictions on IDFA](https://developer.apple.com/app-store/user-privacy-and-data-use/) which will require prompting users to request permission to use IDFA. Therefore, if you intend to set `aaid` to `true` in calls to `identify()` on iOS 14.0 or above, you should expect that the SDK will automatically trigger a user prompt via the `AppTrackingTransparency` framework before it is permitted to send the IDFA value to your DCN. Additionally, we recommend that you ensure to configure the _Privacy - Tracking Usage Description_ attribute string in your application's `Info.plist`, as it enables you to customize some elements of the resulting user prompt. - -The frequency of invocation of `identify` is up to you, however for optimal identity resolution we recommended to call the `identify()` method on your SDK instance every time you authenticate a user, as well as periodically, such as for example once every 15 to 60 minutes while the application is being actively used and an internet connection is available. +### CocoaPods -### Profile API +[CocoaPods](https://cocoapods.org/) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. -To associate key value traits with the device, for eventual audience assembly, you can call the profile API as follows: +To integrate this SDK into your Xcode project using CocoaPods, specify it in your Podfile: -```swift -do { - try OPTABLE!.profile(traits: ["gender": "F", "age": 38, "hasAccount": true]) { result in - switch (result) { - case .success(let response): - // profile API success, response.statusCode is HTTP response status 200 - case .failure(let error): - // handle profile API failure in `error` - } - } -} catch { - // handle thrown exception in `error` -} +```ruby +pod 'OptableSDK' ``` -The specified traits are associated with the user's device and can be matched during audience assembly. +### Carthage -Note that the traits are of type `NSDictionary` and should consist of key value pairs, where the keys are strings and the values are either strings, numbers, or booleans. +[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. -### Targeting API +To integrate this SDK into your Xcode project using Carthage, specify it in your Cartfile: -To get the targeting key values associated by the configured DCN with the device in real-time, you can call the `targeting` API as follows: - -```swift -do { - try OPTABLE!.targeting() { result in - switch result { - case .success(let keyvalues): - // keyvalues is an NSDictionary containing targeting key-values that can be - // passed on to ad servers or other decisioning systems - - case .failure(let error): - // handle targeting API failure in `error` - } - } -} catch { - // handle thrown exception in `error` -} ``` - -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. - -#### 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: - -```swift -let cachedTargetingData = OPTABLE!.targetingFromCache() -if (cachedTargetingData != nil) { - // cachedTargetingData! is an NSDictionary which you can cast as! [String: Any] -} +github "Optable/optable-ios-sdk" ``` -You can also clear the locally cached targeting data: +## Usage -```swift -OPTABLE!.targetingClearCache() -``` - -Note that both `targetingFromCache()` and `targetingClearCache()` are synchronous. - -### Witness API - -To send real-time event data from the user's device to the DCN for eventual audience assembly, you can call the witness API as follows: - -```swift -do { - try OPTABLE!.witness(event: "example.event.type", - properties: ["example": "value"]) { result in - switch (result) { - case .success(let response): - // witness API success, response.statusCode is HTTP response status 200 - case .failure(let error): - // handle witness API failure in `error` - } - } -} catch { - // handle thrown exception in `error` -} -``` - -The specified event type and properties are associated with the logged event and which can be used for matching during audience assembly. - -Note that event properties are of type `NSDictionary` and should consist of key value pairs, where the keys are strings and the values are either strings, numbers, or booleans. - -### Integrating GAM360 - -We can further extend the above `targeting` example to show an integration with a [Google Ad Manager 360](https://admanager.google.com/home/) ad server account. - -It's suggested to load the GAM banner view with an ad even when the call to your DCN `targeting()` method results in failure: +Simplest usage example: ```swift -import GoogleMobileAds -... - -do { - try OPTABLE!.targeting() { result in - var tdata: NSDictionary = [:] - - switch result { - case .success(let keyvalues): - // Save targeting data in `tdata`: - tdata = keyvalues - - case .failure(let error): - // handle targeting API failure in `error` - } - - // We assume bannerView is a DFPBannerView() instance that has already been - // initialized and added to our view: - bannerView.adUnitID = "/12345/some-ad-unit-id/in-your-gam360-account" - - // Build GAM ad request with key values and load banner: - let req = DFPRequest() - req.customTargeting = tdata as! [String: Any] - bannerView.load(req) - } -} catch { - // handle thrown exception in `error` -} -``` +// Configure +let config = OptableConfig(tenant: "dcn.customer.com", originSlug: "my-app") +let optableSDK = OptableSDK(config: config) // can instantiate multiple instances -A working example is available in the demo application. - -## Using (Objective-C) - -Configuring an instance of the `OptableSDK` from an Objective-C application is similar to the above Swift example, except that the caller should set up an `OptableDelegate` protocol delegate. The first step is to implement the delegate itself, for example, in an `OptableSDKDelegate.h`: - -```objective-c -@import OptableSDK; - -@interface OptableSDKDelegate: NSObject -@end -``` - -And in the accompanying `OptableSDKDelegate.m` follows a simple implementation of the delegate calling `NSLog()`: - -```objective-c -#import "OptableSDKDelegate.h" -@import OptableSDK; - -@interface OptableSDKDelegate () -@end - -@implementation OptableSDKDelegate -- (void)identifyOk:(NSHTTPURLResponse *)result { - NSLog(@"Success on identify API call. HTTP Status Code: %ld", result.statusCode); -} -- (void)identifyErr:(NSError *)error { - NSLog(@"Error on identify API call: %@", [error localizedDescription]); -} -- (void)profileOk:(NSHTTPURLResponse *)result { - NSLog(@"Success on profile API call. HTTP Status Code: %ld", result.statusCode); -} -- (void)profileErr:(NSError *)error { - NSLog(@"Error on profile API call: %@", [error localizedDescription]); -} -- (void)targetingOk:(NSDictionary *)result { - NSLog(@"Success on targeting API call: %@", result); -} -- (void)targetingErr:(NSError *)error { - NSLog(@"Error on targeting API call: %@", [error localizedDescription]); -} -- (void)witnessOk:(NSHTTPURLResponse *)result { - NSLog(@"Success on witness API call. HTTP Status Code: %ld", result.statusCode); -} -- (void)witnessErr:(NSError *)error { - NSLog(@"Error on witness API call: %@", [error localizedDescription]); -} -@end -``` - -You can then configure an instance of the SDK integrating with an [Optable](https://optable.co/) DCN running at hostname `dcn.customer.com`, from a configured origin identified by slug `my-app` from your main `AppDelegate.m`, and point it to your delegate implementation as in the following example: - -```objective-c -#import "OptabletSDKDelegate.h" -@import OptableSDK; - -OptableSDK *OPTABLE = nil; -... -@implementation AppDelegate -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - ... - OPTABLE = [[OptableSDK alloc] initWithHost: @"dcn.optable.co" - app: @"ios-sdk-demo" - insecure: NO - useragent: nil]; - OptableSDKDelegate *delegate = [[OptableSDKDelegate alloc] init]; - OPTABLE.delegate = delegate; - ... -} -@end -``` - -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. - -### Identify API - -To associate a user device with an authenticated identifier such as an Email address, or with other known IDs such as the Apple ID for Advertising (IDFA), or even your own vendor or app level `PPID`, you can call the `identify` API as follows: - -```objective-c -@import OptableSDK; -... - -NSError *error = nil; -[OPTABLE identify :@"some.email@address.com" aaid:YES ppid:@"" error:&error]; -``` - -Note that `error` will be set only in case of an internal SDK exception. Otherwise, any configured delegate `identifyOk` or `identifyErr` will be invoked to signal success or failure, respectively. Providing an empty `ppid` as in the above example simply will not send any `ppid`. - -> :warning: **As of iOS 14.0**, Apple has introduced [additional restrictions on IDFA](https://developer.apple.com/app-store/user-privacy-and-data-use/) which will require prompting users to request permission to use IDFA. Therefore, if you intend to set `aaid` to `YES` in calls to `identify` on iOS 14.0 or above, you should expect that the SDK will automatically trigger a user prompt via the `AppTrackingTransparency` framework before it is permitted to send the IDFA value to your DCN. Additionally, we recommend that you ensure to configure the _Privacy - Tracking Usage Description_ attribute string in your application's `Info.plist`, as it enables you to customize some elements of the resulting user prompt. - -It's also possible to send only an Email ID hash or a custom PPID by using the lower-level `identify` method which accepts a list of pre-constructed identifiers, for example: - -```objective-c -@import OptableSDK; -... - -NSError *error = nil; -[OPTABLE identify :@[[OPTABLE cid:@"xyz123abc"], - [OPTABLE eid:@"some.email@address.com" ]] error:&error]; -``` - -### Profile API - -To associate key value traits with the device, for eventual audience assembly, you can call the profile API as follows: - -```objective-c -@import OptableSDK; -... -NSError *error = nil; -[OPTABLE profileWithTraits:@{ @"gender": @"F", @"age": @38, @"hasAccount": @YES } error:&error]; -``` - -### Targeting API - -To get the targeting key values associated by the configured DCN with the device in real-time, you can call the `targeting` API and expect that on success, the resulting keyvalues to be used for targeting will be sent in the `targetingOk` message to your delegate (see the example delegate implementation above): - -```objective-c -@import OptableSDK; -... -NSError *error = nil; -[OPTABLE targetingAndReturnError:&error]; -``` - -#### 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: - -```objective-c -@import OptableSDK; -... -NSDictionary *cachedTargetingData = nil; -cachedTargetingData = [OPTABLE targetingFromCache]; -if (cachedTargetingData != nil) { - // cachedTargetingData! is an NSDictionary -} -``` - -You can also clear the locally cached targeting data: - -```objective-c -@import OptableSDK; -... -[OPTABLE targetingClearCache]; -``` - -Note that both `targetingFromCache` and `targetingClearCache` are synchronous. - -### Witness API - -To send real-time event data from the user's device to the DCN for eventual audience assembly, you can call the witness API as follows: - -```objective-c -@import OptableSDK; -... -NSError *error = nil; -[OPTABLE witness:@"example.event.type" properties:@{ @"example": @"value", @"example2": @123, @"example3": @NO } error:&error]; +// Use +let identifiers = OptableIdentifiers(emailAddress: "test@test.test") +try await optableSDK.identify(identifiers) ``` -### Integrating GAM360 - -We can further extend the above `targetingOk` example delegate implementation to show an integration with a [Google Ad Manager 360](https://admanager.google.com/home/) ad server account, which uses the [Google Mobile Ads SDK's targeting capability](https://developers.google.com/ad-manager/mobile-ads-sdk/ios/targeting). +For more detailed usage guide, see our: -We also extend the `targetingErr` delegate handler to load a GAM ad without targeting data in case of `targeting` API failure. - -```objective-c -@implementation OptableSDKDelegate - ... -- (void)targetingOk:(NSDictionary *)result { - // Update the GAM banner view with result targeting keyvalues: - DFPRequest *request = [DFPRequest request]; - request.customTargeting = result; - [self.bannerView loadRequest:request]; -} -- (void)targetingErr:(NSError *)error { - // Load GAM banner even in case of targeting API error: - DFPRequest *request = [DFPRequest request]; - [self.bannerView loadRequest: request]; -} - ... -@end -``` - -It's assumed in the above code snippet that `self.bannerView` is a pointer to a `DFPBannerView` instance which resides in your delegate and which has already been initialized and configured by a view controller. - -## Identifying visitors arriving from Email newsletters - -If you send Email newsletters that contain links to your application (e.g., universal links), then you may want to automatically _identify_ visitors that have clicked on any such links via their Email address. - -### Insert oeid into your Email newsletter template - -To enable automatic identification of visitors originating from your Email newsletter, you first need to include an **oeid** parameter in the query string of all links to your website in your Email newsletter template. The value of the **oeid** parameter should be set to the SHA256 hash of the lowercased Email address of the recipient. For example, if you are using [Braze](https://www.braze.com/) to send your newsletters, you can easily encode the SHA256 hash value of the recipient's Email address by setting the **oeid** parameter in the query string of any links to your application as follows: - -``` -oeid={{${email_address} | downcase | sha2}} -``` - -The above example uses various personalization tags as documented in [Braze's user guide](https://www.braze.com/docs/user_guide/personalization_and_dynamic_content/) to dynamically insert the required data into an **oeid** parameter, all of which should make up a _part_ of the destination URL in your template. - -### Capture clicks on universal links in your application - -In order for your application to open on devices where it is installed when a link to your domain is clicked, you need to [configure and prepare your application to handle universal links](https://developer.apple.com/ios/universal-links/) first. - -### Call tryIdentifyFromURL SDK API - -When iOS launches your app after a user taps a universal link, you receive an `NSUserActivity` object with an `activityType` value of `NSUserActivityTypeBrowsingWeb`. The activity object's `webpageURL` property contains the URL that the user is accessing. You can then pass it to the SDK's `tryIdentifyFromURL()` API which will automatically look for `oeid` in the query string of the URL and call `identify` with its value if found. - -#### Swift - -```swift -func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { - if userActivity.activityType == NSUserActivityTypeBrowsingWeb { - let url = userActivity.webpageURL! - try OPTABLE!.tryIdentifyFromURL(url) - } - ... -} -``` - -#### Objective-C - -```objective-c --(BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler { - - if ([userActivity.activityType isEqualToString: NSUserActivityTypeBrowsingWeb]) { - NSURL *url = userActivity.webpageURL; - NSError *error = nil; - [OPTABLE tryIdentifyFromURL :url.absoluteString error:&error]; - ... - } - ... - -} -``` +- [Swift integration guide](docs/usage-swift.md) +- [Objective-C integration guide](docs/usage-objc.md) ## Demo Applications -The Swift and Objective-C demo applications show a working example of `identify` , `targeting`, and `witness` APIs, as well as an integration with the [Google Ad Manager 360](https://admanager.google.com/home/) ad server, enabling the targeting of ads served by GAM360 to audiences activated in the [Optable](https://optable.co/) DCN. +The Swift and Objective-C demo applications show a working example of `identify` , `targeting`, `profile` and `witness` APIs, as well as an integration with the [Google Ad Manager 360](https://admanager.google.com/home/) ad server, enabling the targeting of ads served by GAM360 to audiences activated in the [Optable](https://optable.co/) DCN. + +By default, the demo applications will connect to the [Optable](https://optable.co/) demo DCN. -By default, the demo applications will connect to the [Optable](https://optable.co/) demo DCN at `sandbox.optable.co` and reference application slug `android-sdk-demo`. The demo apps depend on the [GAM Mobile Ads SDK for iOS](https://developers.google.com/ad-manager/mobile-ads-sdk/ios/quick-start) and load ads from a GAM360 account operated by [Optable](https://optable.co/). +The demo apps depend on the [GAM Mobile Ads SDK for iOS](https://developers.google.com/ad-manager/mobile-ads-sdk/ios/quick-start) and load ads from a GAM360 account operated by [Optable](https://optable.co/). -### Building +**Build** [Cocoapods](https://cocoapods.org/) is required to build the `demo-ios-swift` and `demo-ios-objc` applications. After cloning the repo, simply `cd` into either of the two demo app directories and run: -``` +```bash +cd demo-ios-swift + +# Install dependencies pod install ``` diff --git a/docs/identify-from-url.md b/docs/identify-from-url.md new file mode 100644 index 0000000..e69de29 diff --git a/releasing.md b/docs/releasing.md similarity index 100% rename from releasing.md rename to docs/releasing.md diff --git a/docs/usage-objc.md b/docs/usage-objc.md new file mode 100644 index 0000000..7a40a9f --- /dev/null +++ b/docs/usage-objc.md @@ -0,0 +1,187 @@ +## Usage (Objective-C) + +Configuring an instance of the `OptableSDK` from an Objective-C application is similar to the above Swift example, except that the caller should set up an `OptableDelegate` protocol delegate. The first step is to implement the delegate itself, for example, in an `OptableSDKDelegate.h`: + +```objective-c +@import OptableSDK; + +@interface OptableSDKDelegate: NSObject +@end +``` + +And in the accompanying `OptableSDKDelegate.m` follows a simple implementation of the delegate calling `NSLog()`: + +```objective-c +#import "OptableSDKDelegate.h" +@import OptableSDK; + +@interface OptableSDKDelegate () +@end + +@implementation OptableSDKDelegate +- (void)identifyOk:(NSHTTPURLResponse *)result { + NSLog(@"Success on identify API call. HTTP Status Code: %ld", result.statusCode); +} +- (void)identifyErr:(NSError *)error { + NSLog(@"Error on identify API call: %@", [error localizedDescription]); +} +- (void)profileOk:(NSHTTPURLResponse *)result { + NSLog(@"Success on profile API call. HTTP Status Code: %ld", result.statusCode); +} +- (void)profileErr:(NSError *)error { + NSLog(@"Error on profile API call: %@", [error localizedDescription]); +} +- (void)targetingOk:(NSDictionary *)result { + NSLog(@"Success on targeting API call: %@", result); +} +- (void)targetingErr:(NSError *)error { + NSLog(@"Error on targeting API call: %@", [error localizedDescription]); +} +- (void)witnessOk:(NSHTTPURLResponse *)result { + NSLog(@"Success on witness API call. HTTP Status Code: %ld", result.statusCode); +} +- (void)witnessErr:(NSError *)error { + NSLog(@"Error on witness API call: %@", [error localizedDescription]); +} +@end +``` + +You can then configure an instance of the SDK integrating with an [Optable](https://optable.co/) DCN running at hostname `dcn.customer.com`, from a configured origin identified by slug `my-app` from your main `AppDelegate.m`, and point it to your delegate implementation as in the following example: + +```objective-c +#import "OptabletSDKDelegate.h" +@import OptableSDK; + +OptableSDK *OPTABLE = nil; +... +@implementation AppDelegate +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + ... + + OptableSDKDelegate *delegate = [OptableSDKDelegate new]; + + OptableConfig *config = [[OptableConfig alloc] initWithTenant: @"prebidtest" originSlug: @"ios-sdk"]; + config.host = @"prebidtest.cloud.optable.co"; + + OPTABLE = [[OptableSDK alloc] initWithConfig: config]; + OPTABLE.delegate = delegate; + + ... +} +@end +``` + +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. + +### Identify API + +To associate a user device with an authenticated identifier such as an Email address, or with other known IDs such as the Apple ID for Advertising (IDFA), or even your own vendor or app level `PPID`, you can call the `identify` API as follows: + +```objective-c +@import OptableSDK; +... +NSError *error = nil; +[OPTABLE identify: @{ @"e" : @"some.email@address.com", @"c" : @"new-custom.ABC" } + error: &error]; +``` + +Note that `error` will be set only in case of an internal SDK exception. Otherwise, any configured delegate `identifyOk` or `identifyErr` will be invoked to signal success or failure, respectively. Providing an empty `ppid` as in the above example simply will not send any `ppid`. + +> :warning: **As of iOS 14.0**, Apple has introduced [additional restrictions on IDFA](https://developer.apple.com/app-store/user-privacy-and-data-use/) which will require prompting users to request permission to use IDFA. Therefore, if you intend to set `aaid` to `YES` in calls to `identify` on iOS 14.0 or above, you should expect that the SDK will automatically trigger a user prompt via the `AppTrackingTransparency` framework before it is permitted to send the IDFA value to your DCN. Additionally, we recommend that you ensure to configure the _Privacy - Tracking Usage Description_ attribute string in your application's `Info.plist`, as it enables you to customize some elements of the resulting user prompt. + +### Profile API + +To associate key value traits with the device, for eventual audience assembly, you can call the profile API as follows: + +```objective-c +@import OptableSDK; +... +NSError *error = nil; +[OPTABLE profileWithTraits: @{ @"gender": @"F", @"age": @38, @"hasAccount": @YES } + id: @"c:2", // NULL-able + neighbors: @[@"c:1", @"c:3"], // NULL-able + error: &error]; +``` + +### Targeting API + +To get the targeting key values associated by the configured DCN with the device in real-time, you can call the `targeting` API and expect that on success, the resulting keyvalues to be used for targeting will be sent in the `targetingOk` message to your delegate (see the example delegate implementation above): + +```objective-c +@import OptableSDK; +... +NSError *error = nil; +[OPTABLE targetingWithIds: @[@"c:1"] // NULL-able + error: &error]; +``` + +#### 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: + +```objective-c +@import OptableSDK; +... +NSDictionary *cachedTargetingData = nil; +cachedTargetingData = [OPTABLE targetingFromCache]; +if (cachedTargetingData != nil) { + // cachedTargetingData! is an NSDictionary +} +``` + +You can also clear the locally cached targeting data: + +```objective-c +@import OptableSDK; +... +[OPTABLE targetingClearCache]; +``` + +Note that both `targetingFromCache` and `targetingClearCache` are synchronous. + +### Witness API + +To send real-time event data from the user's device to the DCN for eventual audience assembly, you can call the witness API as follows: + +```objective-c +@import OptableSDK; +... +NSError *error = nil; +[OPTABLE witnessWithEvent: @"GAMBannerViewController.loadBannerClicked" + properties: @{ @"example": @"value" } + error: &error]; +``` + +### Integrating GAM360 + +We can further extend the above `targetingOk` example delegate implementation to show an integration with a [Google Ad Manager 360](https://admanager.google.com/home/) ad server account, which uses the [Google Mobile Ads SDK's targeting capability](https://developers.google.com/ad-manager/mobile-ads-sdk/ios/targeting). + +We also extend the `targetingErr` delegate handler to load a GAM ad without targeting data in case of `targeting` API failure. + +```objective-c +@implementation OptableSDKDelegate + ... +- (void)targetingOk:(NSDictionary *)result { + // Update the GAM banner view with result targeting keyvalues: + DFPRequest *request = [DFPRequest request]; + request.customTargeting = result; + [self.bannerView loadRequest:request]; +} +- (void)targetingErr:(NSError *)error { + // Load GAM banner even in case of targeting API error: + DFPRequest *request = [DFPRequest request]; + [self.bannerView loadRequest: request]; +} + ... +@end +``` + +It's assumed in the above code snippet that `self.bannerView` is a pointer to a `DFPBannerView` instance which resides in your delegate and which has already been initialized and configured by a view controller. + +### Identifying visitors arriving from Email newsletters + +If you send Email newsletters that contain links to your application (e.g., universal links), then you may want to automatically _identify_ visitors that have clicked on any such links via their Email address. + +- [Check our url identify guide](identify-from-url.md) diff --git a/docs/usage-swift.md b/docs/usage-swift.md new file mode 100644 index 0000000..3ddd360 --- /dev/null +++ b/docs/usage-swift.md @@ -0,0 +1,228 @@ +## Usage (Swift) + +To configure an instance of the SDK integrating with an [Optable](https://optable.co/) DCN running at hostname `dcn.customer.com`, from a configured Swift application origin identified by slug `my-app`, you simply create an instance of the `OptableSDK` class through which you can communicate to the DCN. For example, from your `AppDelegate`: + +```swift +import OptableSDK +import UIKit +... + +var OPTABLE: OptableSDK? + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: + [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + ... + let config = OptableConfig( + tenant: "dcn.customer.com", + originSlug: "my-app", + host: "dcn.customer.com" + ) + OPTABLE = OptableSDK(config: config) + ... + return true + } + ... +} +``` + +Note that while the `OPTABLE` variable is global, we initialize it with an instance of `OptableSDK` in the `application()` method which runs at app launch, and not at the time it is declared. This is done because Swift's lazy-loading otherwise delays initialization to the first use of the variable. Both approaches work, though forcing early initialization allows the SDK to configure itself early. In particular, as part of its internal configuration the SDK will attempt to read the User-Agent string exposed by WebView and, since this is an asynchronous operation, it is best done as early as possible in the application lifecycle. + +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 all SDK communication with Optable DCNs is done over TLS. The only exception to this is if you instantiate the `OptableSDK` class with a third optional boolean parameter, `insecure`, set to `true`. For example: + +```swift +let config = OptableConfig(..., insecure: true) +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: + +```swift +let config = OptableConfig(..., useragent: "custom-ua") +OPTABLE = OptableSDK(config: config) +``` + +The default value of `nil` for the `useragent` parameter enables the `WKWebView` auto-detection behavior. + +### Identify API + +To associate a user device with an authenticated identifier such as an Email address, or with other known IDs such as the Apple ID for Advertising (IDFA), or even your own vendor or app level `PPID`, you can call the `identify` API as follows: + +```swift +let emailString = "some.email@address.com" + +do { + let identifiers = OptableIdentifiers(emailAddress: emailString) + try OPTABLE!.identify(identifiers) { result in + switch (result) { + case .success(let response): + // identify API success, response.statusCode is HTTP response status 200 + case .failure(let error): + // handle identify API failure in `error` + } + } +} catch { + // handle thrown exception in `error` +} +``` + +The SDK `identify()` method will asynchronously connect to the configured DCN and send IDs for resolution. The provided callback can be used to understand successful completion or errors. + +> :warning: **Client-Side Hashing**: The SDK will compute the SHA-256 hash of the email address and phone number on the client-side and send the hashed value to the DCN. +> +> The email address / phone number is **not** sent by the device in plain text. + +Since the `sendIDFA` value provided to `identify()` via the `aaid` (Apple Advertising ID or IDFA) boolean parameter is `true`, the SDK will attempt to fetch and send the Apple IDFA in the call to `identify` too, unless the user has turned on "Limit ad tracking" in their iOS device privacy settings. + +> :warning: **As of iOS 14.0**, Apple has introduced [additional restrictions on IDFA](https://developer.apple.com/app-store/user-privacy-and-data-use/) which will require prompting users to request permission to use IDFA. Therefore, if you intend to set `aaid` to `true` in calls to `identify()` on iOS 14.0 or above, you should expect that the SDK will automatically trigger a user prompt via the `AppTrackingTransparency` framework before it is permitted to send the IDFA value to your DCN. Additionally, we recommend that you ensure to configure the _Privacy - Tracking Usage Description_ attribute string in your application's `Info.plist`, as it enables you to customize some elements of the resulting user prompt. + +The frequency of invocation of `identify` is up to you, however for optimal identity resolution we recommended to call the `identify()` method on your SDK instance every time you authenticate a user, as well as periodically, such as for example once every 15 to 60 minutes while the application is being actively used and an internet connection is available. + +### Profile API + +> :information_source: For more info check: +> [Optable Real-Time API Endpoints > Profile](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide/optable-real-time-api-endpoints/profile) + +To associate key value traits with the device, for eventual audience assembly, you can call the profile API as follows: + +```swift +do { + try OPTABLE!.profile(traits: ["gender": "F", "age": 38, "hasAccount": true]) { result in + switch (result) { + case .success(let response): + // profile API success, response.statusCode is HTTP response status 200 + case .failure(let error): + // handle profile API failure in `error` + } + } +} catch { + // handle thrown exception in `error` +} +``` + +The specified traits are associated with the user's device and can be matched during audience assembly. + +Note that the traits are of type `NSDictionary` and should consist of key value pairs, where the keys are strings and the values are either strings, numbers, or booleans. + +### Targeting API + +> :information_source: For more info check: +> [Optable Real-Time API Endpoints > Targeting](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide/optable-real-time-api-endpoints/targeting) + +To get the targeting key values associated by the configured DCN with the device in real-time, you can call the `targeting` API as follows: + +```swift +do { + try OPTABLE!.targeting() { result in + switch result { + case .success(let keyvalues): + // keyvalues is an NSDictionary containing targeting key-values that can be + // passed on to ad servers or other decisioning systems + + case .failure(let error): + // handle targeting API failure in `error` + } + } +} catch { + // handle thrown exception in `error` +} +``` + +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. + +#### 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: + +```swift +let cachedTargetingData = OPTABLE!.targetingFromCache() +if (cachedTargetingData != nil) { + // cachedTargetingData! is an NSDictionary which you can cast as! [String: Any] +} +``` + +You can also clear the locally cached targeting data: + +```swift +OPTABLE!.targetingClearCache() +``` + +Note that both `targetingFromCache()` and `targetingClearCache()` are synchronous. + +### Witness API + +> :information_source: For more info check: +> [Optable Real-Time API Endpoints > Witness](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide/optable-real-time-api-endpoints/) + +To send real-time event data from the user's device to the DCN for eventual audience assembly, you can call the witness API as follows: + +```swift +do { + try OPTABLE!.witness(event: "example.event.type", properties: ["example": "value"]) { result in + switch (result) { + case .success(let response): + // witness API success, response.statusCode is HTTP response status 200 + case .failure(let error): + // handle witness API failure in `error` + } + } +} catch { + // handle thrown exception in `error` +} +``` + +The specified event type and properties are associated with the logged event and which can be used for matching during audience assembly. + +Note that event properties are of type `NSDictionary` and should consist of key value pairs, where the keys are strings and the values are either strings, numbers, or booleans. + +### Integrating GAM360 + +We can further extend the above `targeting` example to show an integration with a [Google Ad Manager 360](https://admanager.google.com/home/) ad server account. + +It's suggested to load the GAM banner view with an ad even when the call to your DCN `targeting()` method results in failure: + +```swift +import GoogleMobileAds +... + +do { + try OPTABLE!.targeting() { result in + var tdata: NSDictionary = [:] + + switch result { + case .success(let keyvalues): + // Save targeting data in `tdata`: + tdata = keyvalues + + case .failure(let error): + // handle targeting API failure in `error` + } + + // We assume bannerView is a DFPBannerView() instance that has already been + // initialized and added to our view: + bannerView.adUnitID = "/12345/some-ad-unit-id/in-your-gam360-account" + + // Build GAM ad request with key values and load banner: + let req = DFPRequest() + req.customTargeting = tdata as! [String: Any] + bannerView.load(req) + } +} catch { + // handle thrown exception in `error` +} +``` + +A working example is available in the demo application. + +### Identifying visitors arriving from Email newsletters + +If you send Email newsletters that contain links to your application (e.g., universal links), then you may want to automatically _identify_ visitors that have clicked on any such links via their Email address. + +- [Check our url identify guide](identify-from-url.md) From d30f5258772a54920f9a9a2a2bd99376b6c2bffb Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 22 Dec 2025 16:27:25 +0100 Subject: [PATCH 38/42] - updated docs/identify-from-url.md --- docs/identify-from-url.md | 51 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/identify-from-url.md b/docs/identify-from-url.md index e69de29..70582a7 100644 --- a/docs/identify-from-url.md +++ b/docs/identify-from-url.md @@ -0,0 +1,51 @@ +## Identifying visitors arriving from Email newsletters + +If you send Email newsletters that contain links to your application (e.g., universal links), then you may want to automatically _identify_ visitors that have clicked on any such links via their Email address. + +### Insert oeid into your Email newsletter template + +To enable automatic identification of visitors originating from your Email newsletter, you first need to include an **oeid** parameter in the query string of all links to your website in your Email newsletter template. The value of the **oeid** parameter should be set to the SHA256 hash of the lowercased Email address of the recipient. For example, if you are using [Braze](https://www.braze.com/) to send your newsletters, you can easily encode the SHA256 hash value of the recipient's Email address by setting the **oeid** parameter in the query string of any links to your application as follows: + +``` +oeid={{${email_address} | downcase | sha2}} +``` + +The above example uses various personalization tags as documented in [Braze's user guide](https://www.braze.com/docs/user_guide/personalization_and_dynamic_content/) to dynamically insert the required data into an **oeid** parameter, all of which should make up a _part_ of the destination URL in your template. + +### Capture clicks on universal links in your application + +In order for your application to open on devices where it is installed when a link to your domain is clicked, you need to [configure and prepare your application to handle universal links](https://developer.apple.com/ios/universal-links/) first. + +### Call tryIdentifyFromURL SDK API + +When iOS launches your app after a user taps a universal link, you receive an `NSUserActivity` object with an `activityType` value of `NSUserActivityTypeBrowsingWeb`. The activity object's `webpageURL` property contains the URL that the user is accessing. You can then pass it to the SDK's `tryIdentifyFromURL()` API which will automatically look for `oeid` in the query string of the URL and call `identify` with its value if found. + +#### Swift + +```swift +func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { + if userActivity.activityType == NSUserActivityTypeBrowsingWeb { + let url = userActivity.webpageURL! + try OPTABLE!.tryIdentifyFromURL(url) + ~#TODO#~ + } + ... +} +``` + +#### Objective-C + +```objective-c +-(BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler { + + if ([userActivity.activityType isEqualToString: NSUserActivityTypeBrowsingWeb]) { + NSURL *url = userActivity.webpageURL; + NSError *error = nil; + [OPTABLE tryIdentifyFromURL :url.absoluteString error:&error]; + ~#TODO#~ + ... + } + ... + +} +``` From 173e7b995b9463c52bc8dd05e4090b0ad1113a94 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 22 Dec 2025 16:55:11 +0100 Subject: [PATCH 39/42] - Updated OptableSDK.eidFromURL(_:) => OptableSDK.tryIdentifyFromURL(_:) --- Source/OptableIdentifiers.swift | 13 +++++++++++++ Source/OptableSDK.swift | 18 ++++++++++++++++-- Tests/Unit/OptableIdentifiersTests.swift | 22 ++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Source/OptableIdentifiers.swift b/Source/OptableIdentifiers.swift index 6304655..ac11d72 100644 --- a/Source/OptableIdentifiers.swift +++ b/Source/OptableIdentifiers.swift @@ -68,6 +68,19 @@ public struct OptableIdentifiers { get { dict[key.rawValue] } set { dict[key.rawValue] = newValue } } + + public init(_ array: [String]) { + for item in array { + if let colonIndex = item.firstIndex(of: ":"), colonIndex > item.startIndex { + let prefix = String(item[.. [String] { var results: [String] = [] diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index 4dd1474..cc955b5 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -294,8 +294,22 @@ public extension OptableSDK { // MARK: - Identify from URL public extension OptableSDK { - func eidFromURL(_ urlString: String) -> String { - OptableIdentifierEncoder.eidFromURL(urlString) + /// + /// tryIdentifyFromURL(urlString) is a helper that attempts to find a valid-looking + /// "oeid" parameter in the specified urlString's query string parameters and, if found, + /// calls self.identify([oeid]). + /// + /// The use for this is when handling incoming universal links which might contain an + /// "oeid" value with the SHA256(downcase(email)) of an incoming user, such as encoded + /// links in newsletter Emails sent by the application developer. + /// + @objc + func tryIdentifyFromURL(_ urlString: String) throws { + let oeid = OptableIdentifierEncoder.eidFromURL(urlString) + + guard oeid.isEmpty == false else { return } + + try self._identify(OptableIdentifiers([oeid]), completion: { _ in /* no-op */ }) } } diff --git a/Tests/Unit/OptableIdentifiersTests.swift b/Tests/Unit/OptableIdentifiersTests.swift index cf6ef2a..0452e89 100644 --- a/Tests/Unit/OptableIdentifiersTests.swift +++ b/Tests/Unit/OptableIdentifiersTests.swift @@ -63,6 +63,28 @@ class OptableIdentifiersTests: XCTestCase { ]) try test_json_generation_list(oids: oids) } + + func test_json_generation_list_raw_array() throws { + let oids = OptableIdentifiers([ + "e:foo@bar.com", + "p:+15123465890", + "z:M5V 3L9", + "i4:8.8.8.8", + "i6:2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "a:496f5db5-681f-4392-acd5-0d4f6e2f6b88", + "g:64873d9f-d5af-4770-8bcb-167a220eb17d", + "r:0b179df0-6cd5-49f1-be21-425d002e0d22", + "s:e0ef86a8-6ebf-4c9d-9127-e69407fe748d", + "f:6e853799-ef31-4a30-8706-9742be254d38", + "n:_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ", + "id5:ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg", + "utiq:496f5db5-681f-4392-acd5-0d4f6e2f6b88", + "c:d29c551097b9dd0b82423827f65161232efaf7fc", + "c1:AaaZza.dh012", + "c2:", + ]) + try test_json_generation_list(oids: oids) + } func test_json_generation_list_enum_dict() throws { let oids = OptableIdentifiers([ From d9ea109e3321f399d63c86d5a3d6ccaefdb80339 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 22 Dec 2025 16:55:35 +0100 Subject: [PATCH 40/42] - Updated docs/identify-from-url --- docs/identify-from-url.md | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/docs/identify-from-url.md b/docs/identify-from-url.md index 70582a7..465c4ff 100644 --- a/docs/identify-from-url.md +++ b/docs/identify-from-url.md @@ -24,12 +24,11 @@ When iOS launches your app after a user taps a universal link, you receive an `N ```swift func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { - if userActivity.activityType == NSUserActivityTypeBrowsingWeb { - let url = userActivity.webpageURL! - try OPTABLE!.tryIdentifyFromURL(url) - ~#TODO#~ - } - ... + if userActivity.activityType == NSUserActivityTypeBrowsingWeb { + let url = userActivity.webpageURL! + try OPTABLE!.tryIdentifyFromURL(url) + } + ... } ``` @@ -38,14 +37,11 @@ func application(_ application: UIApplication, continue userActivity: NSUserActi ```objective-c -(BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler { - if ([userActivity.activityType isEqualToString: NSUserActivityTypeBrowsingWeb]) { - NSURL *url = userActivity.webpageURL; - NSError *error = nil; - [OPTABLE tryIdentifyFromURL :url.absoluteString error:&error]; - ~#TODO#~ + if ([userActivity.activityType isEqualToString: NSUserActivityTypeBrowsingWeb]) { + NSURL *url = userActivity.webpageURL; + NSError *error = nil; + [OPTABLE tryIdentifyFromURL: url.absoluteString error: &error]; + } ... - } - ... - } ``` From 3cc878b64101834f338e47184885e406cb35d010 Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 22 Dec 2025 18:11:45 +0100 Subject: [PATCH 41/42] - Updated demo-ios-swift with prebid example --- demo-ios-swift/Podfile | 5 +- demo-ios-swift/Podfile.lock | 8 +- .../demo-ios-swift.xcodeproj/project.pbxproj | 12 ++ .../demo-ios-swift/AppDelegate.swift | 18 ++ .../Base.lproj/LaunchScreen.storyboard | 13 +- .../demo-ios-swift/Base.lproj/Main.storyboard | 117 ++++++++++- .../GAMBannerViewController.swift | 102 +++++---- .../IdentifyViewController.swift | 2 +- .../PrebidBannerViewController.swift | 197 ++++++++++++++++++ 9 files changed, 409 insertions(+), 65 deletions(-) create mode 100644 demo-ios-swift/demo-ios-swift/PrebidBannerViewController.swift diff --git a/demo-ios-swift/Podfile b/demo-ios-swift/Podfile index 82b47c6..5e9d12e 100644 --- a/demo-ios-swift/Podfile +++ b/demo-ios-swift/Podfile @@ -1,9 +1,5 @@ platform :ios, '15.0' -source 'https://cdn.cocoapods.org/' - -project 'demo-ios-swift.xcodeproj' - target 'demo-ios-swift' do use_frameworks! @@ -12,5 +8,6 @@ target 'demo-ios-swift' do #pod 'OptableSDK' pod 'Google-Mobile-Ads-SDK' + pod 'PrebidMobile' end diff --git a/demo-ios-swift/Podfile.lock b/demo-ios-swift/Podfile.lock index fa3644b..0624621 100644 --- a/demo-ios-swift/Podfile.lock +++ b/demo-ios-swift/Podfile.lock @@ -3,15 +3,20 @@ PODS: - GoogleUserMessagingPlatform (>= 1.1) - GoogleUserMessagingPlatform (3.1.0) - OptableSDK (0.10.0) + - PrebidMobile (3.1.0): + - PrebidMobile/core (= 3.1.0) + - PrebidMobile/core (3.1.0) DEPENDENCIES: - Google-Mobile-Ads-SDK - OptableSDK (from `../`) + - PrebidMobile SPEC REPOS: trunk: - Google-Mobile-Ads-SDK - GoogleUserMessagingPlatform + - PrebidMobile EXTERNAL SOURCES: OptableSDK: @@ -21,7 +26,8 @@ SPEC CHECKSUMS: Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2 GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c OptableSDK: fc5d3852c29fac1881b1d3ab6ea397de71c8cbf1 + PrebidMobile: 046bb6220157c7332dc6c6e19a99397bb481ac3a -PODFILE CHECKSUM: b1a602894bc694156e8907d24cabf069be3c8bee +PODFILE CHECKSUM: aca8225c99e7af1a76b3862a46118652667f5944 COCOAPODS: 1.16.2 diff --git a/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj b/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj index a277065..6063a80 100644 --- a/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj +++ b/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 3E8C4D28A821E9F0A202EA9D /* Pods_demo_ios_swift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D76144A328FE0069ABDF4B5F /* Pods_demo_ios_swift.framework */; }; + 536D9E922EA0DD37006D86BE /* PrebidBannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D9E912EA0DD37006D86BE /* PrebidBannerViewController.swift */; }; 631466ED24F7F555007DCA5D /* GAMBannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631466EC24F7F555007DCA5D /* GAMBannerViewController.swift */; }; 6352AA5B24DC7AE9002E66EB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6352AA5A24DC7AE9002E66EB /* AppDelegate.swift */; }; 6352AA5D24DC7AE9002E66EB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6352AA5C24DC7AE9002E66EB /* SceneDelegate.swift */; }; @@ -33,6 +34,7 @@ /* Begin PBXFileReference section */ 0A022CEF25F2CA644724048D /* Pods_demo_ios_swiftTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_demo_ios_swiftTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3386157F1FDF211F3621C6CF /* Pods-demo-ios-swift-demo-ios-swiftUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-swift-demo-ios-swiftUITests.debug.xcconfig"; path = "Target Support Files/Pods-demo-ios-swift-demo-ios-swiftUITests/Pods-demo-ios-swift-demo-ios-swiftUITests.debug.xcconfig"; sourceTree = ""; }; + 536D9E912EA0DD37006D86BE /* PrebidBannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidBannerViewController.swift; sourceTree = ""; }; 631466EC24F7F555007DCA5D /* GAMBannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GAMBannerViewController.swift; sourceTree = ""; }; 6352AA5724DC7AE9002E66EB /* demo-ios-swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "demo-ios-swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 6352AA5A24DC7AE9002E66EB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -87,6 +89,7 @@ 6352AA5A24DC7AE9002E66EB /* AppDelegate.swift */, 6352AA5C24DC7AE9002E66EB /* SceneDelegate.swift */, 631466EC24F7F555007DCA5D /* GAMBannerViewController.swift */, + 536D9E912EA0DD37006D86BE /* PrebidBannerViewController.swift */, 6352AA5E24DC7AE9002E66EB /* IdentifyViewController.swift */, 6352AA6024DC7AE9002E66EB /* Main.storyboard */, 6352AA6324DC7AEC002E66EB /* Assets.xcassets */, @@ -220,10 +223,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift-frameworks.sh\"\n"; @@ -237,10 +244,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift-resources.sh\"\n"; @@ -254,6 +265,7 @@ buildActionMask = 2147483647; files = ( 6352AA5F24DC7AE9002E66EB /* IdentifyViewController.swift in Sources */, + 536D9E922EA0DD37006D86BE /* PrebidBannerViewController.swift in Sources */, 6352AA5B24DC7AE9002E66EB /* AppDelegate.swift in Sources */, 631466ED24F7F555007DCA5D /* GAMBannerViewController.swift in Sources */, 6352AA5D24DC7AE9002E66EB /* SceneDelegate.swift in Sources */, diff --git a/demo-ios-swift/demo-ios-swift/AppDelegate.swift b/demo-ios-swift/demo-ios-swift/AppDelegate.swift index b0f0c46..e0fd9ff 100644 --- a/demo-ios-swift/demo-ios-swift/AppDelegate.swift +++ b/demo-ios-swift/demo-ios-swift/AppDelegate.swift @@ -9,6 +9,9 @@ import OptableSDK import UIKit +import GoogleMobileAds +import PrebidMobile + // The OPTABLE global points to an instance of OptableSDK which is initialized in the AppDelegate application() method at app launch. // While we could have initialized the global directly here, due to Swift lazy-loading this would delay initialization to the first // use of the SDK. While not strictly required, we want to force early initialization so that the SDK can detect the correct useragent @@ -29,10 +32,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate { skipAdvertisingIdDetection: false ) OPTABLE = OptableSDK(config: config) + + initPrebidMobile() + initGoogleMobileAds() return true } + private func initPrebidMobile() { + Prebid.shared.prebidServerAccountId = "0689a263-318d-448b-a3d4-b02e8a709d9d" + + try? Prebid.initializeSDK( + serverURL: "https://prebid-server-test-j.prebid.org/openrtb2/auction" + ) + } + + private func initGoogleMobileAds() { + MobileAds.shared.start() + } + // MARK: UISceneSession Lifecycle func application( diff --git a/demo-ios-swift/demo-ios-swift/Base.lproj/LaunchScreen.storyboard b/demo-ios-swift/demo-ios-swift/Base.lproj/LaunchScreen.storyboard index 3ac9e6f..1e9f3a1 100644 --- a/demo-ios-swift/demo-ios-swift/Base.lproj/LaunchScreen.storyboard +++ b/demo-ios-swift/demo-ios-swift/Base.lproj/LaunchScreen.storyboard @@ -1,9 +1,11 @@ - + - + + + @@ -14,8 +16,8 @@ - + @@ -23,4 +25,9 @@ + + + + + diff --git a/demo-ios-swift/demo-ios-swift/Base.lproj/Main.storyboard b/demo-ios-swift/demo-ios-swift/Base.lproj/Main.storyboard index 1bf46bf..d4f9299 100644 --- a/demo-ios-swift/demo-ios-swift/Base.lproj/Main.storyboard +++ b/demo-ios-swift/demo-ios-swift/Base.lproj/Main.storyboard @@ -1,6 +1,6 @@ - - + + @@ -204,6 +204,7 @@ + @@ -232,6 +233,7 @@ + @@ -244,10 +246,117 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -259,7 +368,7 @@ - + diff --git a/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift b/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift index 4a8fa0d..886bb35 100644 --- a/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift +++ b/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift @@ -9,12 +9,14 @@ import UIKit import GoogleMobileAds +fileprivate let AD_MANAGER_AD_UNIT_ID = "/22081946781/ios-sdk-demo/mobile-leaderboard" + class GAMBannerViewController: UIViewController { + // MARK: - GoogleMobileAds var bannerView: BannerView! - // MARK: Properties - + // MARK: - Outlets @IBOutlet weak var adPlaceholder: UIView! @IBOutlet weak var loadBannerButton: UIButton! @IBOutlet weak var loadBannerFromCacheButton: UIButton! @@ -27,35 +29,28 @@ class GAMBannerViewController: UIViewController { bannerView = BannerView(adSize: AdSizeBanner) addBannerViewToView(bannerView) bannerView.rootViewController = self + bannerView.adUnitID = AD_MANAGER_AD_UNIT_ID } - //MARK: Actions - @IBAction func loadBannerWithTargeting(_ sender: UIButton) { + setOutput("📡 Calling /targeting API...\n\n") + do { - targetingOutput.text = "Calling /targeting API...\n\n" - - try OPTABLE!.targeting() { result in + try OPTABLE!.targeting() { [weak self] result in var tdata: NSDictionary = [:] switch result { case .success(let keyvalues): print("[OptableSDK] Success on /targeting API call: \(keyvalues)") - tdata = keyvalues - - DispatchQueue.main.async { - self.targetingOutput.text += "Data: \(keyvalues)\n" - } + self?.appendOutput("✅ Data: \(keyvalues)\n") case .failure(let error): print("[OptableSDK] Error on /targeting API call: \(error)") - DispatchQueue.main.async { - self.targetingOutput.text += "🚫 Error: \(error)\n" - } + self?.appendOutput("🚫 Error: \(error)\n") } - self.loadBanner(adUnitID: "/22081946781/ios-sdk-demo/mobile-leaderboard", keyvalues: tdata) + self?.loadBanner(keyvalues: tdata) } } catch { print("[OptableSDK] Exception: \(error)") @@ -63,30 +58,28 @@ class GAMBannerViewController: UIViewController { } @IBAction func loadBannerWithTargetingFromCache(_ sender: UIButton) { - var tdata: NSDictionary = [:] - - targetingOutput.text = "Checking local targeting cache...\n\n" + setOutput("🗂 Checking local targeting cache...\n\n") + var tdata: NSDictionary = [:] let cachedValues = OPTABLE!.targetingFromCache() - if (cachedValues != nil) { - print("[OptableSDK] Cached targeting values found: \(cachedValues!)") - targetingOutput.text += "\nFound cached data: \(cachedValues!)\n" - tdata = cachedValues! + + if let cachedValues { + print("[OptableSDK] Cached targeting values found: \(cachedValues)") + appendOutput("✅ Found cached data: \(cachedValues)\n") + tdata = cachedValues } else { - targetingOutput.text += "\nCache empty.\n" + appendOutput("ℹ️ Cache empty.\n") } - self.loadBanner(adUnitID: "/22081946781/ios-sdk-demo/mobile-leaderboard", keyvalues: tdata) + loadBanner(keyvalues: tdata) } @IBAction func clearTargetingCache(_ sender: UIButton) { - targetingOutput.text = "🧹 Clearing local targeting cache.\n" + setOutput("🧹 Clearing local targeting cache.\n") OPTABLE!.targetingClearCache() } - - private func loadBanner(adUnitID: String, keyvalues: NSDictionary) { - bannerView.adUnitID = adUnitID - + + private func loadBanner(keyvalues: NSDictionary) { let req = AdManagerRequest() req.customTargeting = keyvalues as? [String: String] bannerView.load(req) @@ -97,19 +90,15 @@ class GAMBannerViewController: UIViewController { private func witness() { do { - try OPTABLE!.witness(event: "GAMBannerViewController.loadBannerClicked", properties: ["example": "value"]) { result in + try OPTABLE!.witness( + event: "GAMBannerViewController.loadBannerClicked", + properties: ["example": "value"] + ) { [weak self] result in switch result { - case .success(let response): - print("[OptableSDK] Success on /witness API call: response.statusCode = \(response.statusCode)") - DispatchQueue.main.async { - self.targetingOutput.text += "\n✅ Success calling witness API to log loadBannerClicked event.\n" - } - + case .success: + self?.appendOutput("\n✅ Success calling witness API to log loadBannerClicked event.\n") case .failure(let error): - print("[OptableSDK] Error on /witness API call: \(error)") - DispatchQueue.main.async { - self.targetingOutput.text += "\n🚫 Error: \(error)" - } + self?.appendOutput("\n🚫 Error: \(error)\n") } } } catch { @@ -119,19 +108,14 @@ class GAMBannerViewController: UIViewController { private func profile() { do { - try OPTABLE!.profile(traits: ["example": "value", "anotherExample": 123, "thirdExample": true ]) { result in + try OPTABLE!.profile( + traits: ["example": "value", "anotherExample": 123, "thirdExample": true] + ) { [weak self] result in switch result { - case .success(let response): - print("[OptableSDK] Success on /profile API call: response.statusCode = \(response.statusCode)") - DispatchQueue.main.async { - self.targetingOutput.text += "\n✅ Success calling profile API to set example traits.\n" - } - + case .success: + self?.appendOutput("\n✅ Success calling profile API to set example traits.\n") case .failure(let error): - print("[OptableSDK] Error on /profile API call: \(error)") - DispatchQueue.main.async { - self.targetingOutput.text += "\n🚫 Error: \(error)" - } + self?.appendOutput("\n🚫 Error: \(error)\n") } } } catch { @@ -139,6 +123,20 @@ class GAMBannerViewController: UIViewController { } } + // MARK: - Helpers + + private func setOutput(_ text: String) { + DispatchQueue.main.async { + self.targetingOutput.text = text + } + } + + private func appendOutput(_ text: String) { + DispatchQueue.main.async { + self.targetingOutput.text += text + } + } + private func addBannerViewToView(_ bannerView: BannerView) { bannerView.translatesAutoresizingMaskIntoConstraints = false adPlaceholder.addSubview(bannerView) diff --git a/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift b/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift index 46a0dd4..865d69f 100644 --- a/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift +++ b/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift @@ -6,8 +6,8 @@ // See LICENSE for details. // -import UIKit import OptableSDK +import UIKit class IdentifyViewController: UIViewController { // MARK: Properties diff --git a/demo-ios-swift/demo-ios-swift/PrebidBannerViewController.swift b/demo-ios-swift/demo-ios-swift/PrebidBannerViewController.swift new file mode 100644 index 0000000..5d6b2d3 --- /dev/null +++ b/demo-ios-swift/demo-ios-swift/PrebidBannerViewController.swift @@ -0,0 +1,197 @@ +// +// PrebidBannerViewController.swift +// demo-ios-swift +// +// Copyright © 2020 Optable Technologies Inc. All rights reserved. +// See LICENSE for details. +// + +import UIKit +import PrebidMobile +import GoogleMobileAds + +fileprivate let AD_MANAGER_AD_UNIT_ID = "/21808260008/prebid_demo_app_original_api_banner" +fileprivate let PREBID_STORED_IMP = "prebid-demo-banner-320-50" + +class PrebidBannerViewController: UIViewController { + + // MARK: - PrebidMobile + private var pbmBannerAdUnit: BannerAdUnit! + + // MARK: - GoogleMobileAds + private var adManagerBannerView: AdManagerBannerView! + + // MARK: - Outlets + @IBOutlet weak var adPlaceholder: UIView! + @IBOutlet weak var loadBannerButton: UIButton! + @IBOutlet weak var loadBannerFromCacheButton: UIButton! + @IBOutlet weak var clearTargetingCacheButton: UIButton! + @IBOutlet weak var targetingOutput: UITextView! + + override func viewDidLoad() { + super.viewDidLoad() + + pbmBannerAdUnit = BannerAdUnit( + configId: PREBID_STORED_IMP, + size: .init(width: 320, height: 50) + ) + + adManagerBannerView = AdManagerBannerView(adSize: AdSizeBanner) + adManagerBannerView.adUnitID = AD_MANAGER_AD_UNIT_ID + adManagerBannerView.rootViewController = self + adManagerBannerView.delegate = self + addBannerViewToView(adManagerBannerView) + } + + @IBAction func loadBannerWithTargeting(_ sender: UIButton) { + setOutput("📡 Calling /targeting API...\n\n") + + do { + try OPTABLE!.targeting { [weak self] result in + var tdata: NSDictionary = [:] + + switch result { + case .success(let keyvalues): + print("[OptableSDK] Success on /targeting API call: \(keyvalues)") + tdata = keyvalues + self?.appendOutput("✅ Targeting data:\n\(keyvalues)\n") + + case .failure(let error): + print("[OptableSDK] Error on /targeting API call: \(error)") + self?.appendOutput("🚫 Error: \(error.localizedDescription)\n") + } + + self?.loadBanner(keyvalues: tdata) + } + } catch { + print("[OptableSDK] Exception: \(error)") + appendOutput("⚠️ Exception: \(error.localizedDescription)\n") + } + } + + @IBAction func loadBannerWithTargetingFromCache(_ sender: UIButton) { + setOutput("🗂 Checking local targeting cache...\n\n") + + var tdata: NSDictionary = [:] + if let cachedValues = OPTABLE!.targetingFromCache() { + print("[OptableSDK] Cached targeting values found: \(cachedValues)") + appendOutput("✅ Found cached data:\n\(cachedValues)\n") + tdata = cachedValues + } else { + appendOutput("ℹ️ Cache empty.\n") + } + + loadBanner(keyvalues: tdata) + } + + @IBAction func clearTargetingCache(_ sender: UIButton) { + setOutput("🧹 Clearing local targeting cache.\n") + OPTABLE!.targetingClearCache() + } + + private func loadBanner(keyvalues: NSDictionary) { + let request = AdManagerRequest() + + pbmBannerAdUnit.fetchDemand(adObject: request) { [weak self] status in + if status != .prebidDemandFetchSuccess { + print("[PrebidMobile SDK] Prebid fetch demand was not successful: \(status.name())") + } + + // TODO: - Where should keyvalues go in Prebid and GAM(?) ? + if let keyvalues = keyvalues as? [String: String] { + request.customTargeting?.merge(keyvalues, uniquingKeysWith: { $1 }) + } + + self?.adManagerBannerView.load(request) + } + + witness() + profile() + } + + private func witness() { + do { + try OPTABLE!.witness( + event: "PrebidBannerViewController.loadBannerClicked", + properties: ["example": "value"] + ) { [weak self] result in + switch result { + case .success(let response): + print("[OptableSDK] Witness success: \(response.statusCode)") + self?.appendOutput("✅ Witness API logged loadBannerClicked event.\n") + case .failure(let error): + print("[OptableSDK] Witness error: \(error)") + self?.appendOutput("🚫 Witness error: \(error.localizedDescription)\n") + } + } + } catch { + print("[OptableSDK] Exception: \(error)") + appendOutput("⚠️ Witness exception: \(error.localizedDescription)\n") + } + } + + private func profile() { + do { + try OPTABLE!.profile( + traits: ["example": "value", "anotherExample": 123, "thirdExample": true] + ) { [weak self] result in + switch result { + case .success(let response): + print("[OptableSDK] Profile success: \(response.statusCode)") + self?.appendOutput("✅ Profile API set example traits.\n") + case .failure(let error): + print("[OptableSDK] Profile error: \(error)") + self?.appendOutput("🚫 Profile error: \(error.localizedDescription)\n") + } + } + } catch { + print("[OptableSDK] Exception: \(error)") + appendOutput("⚠️ Profile exception: \(error.localizedDescription)\n") + } + } + + // MARK: - Helpers + private func addBannerViewToView(_ bannerView: AdManagerBannerView) { + bannerView.translatesAutoresizingMaskIntoConstraints = false + adPlaceholder.addSubview(bannerView) + + NSLayoutConstraint.activate([ + bannerView.centerXAnchor.constraint(equalTo: adPlaceholder.centerXAnchor), + bannerView.centerYAnchor.constraint(equalTo: adPlaceholder.centerYAnchor) + ]) + } + + /// Safely sets the full text of targetingOutput on the main thread. + private func setOutput(_ text: String) { + DispatchQueue.main.async { + self.targetingOutput.text = text + } + } + + /// Appends text to targetingOutput on the main thread with line break. + private func appendOutput(_ text: String) { + DispatchQueue.main.async { + self.targetingOutput.text += "\n\(text)" + } + } +} + +// MARK: - GoogleMobileAds.BannerViewDelegate +extension PrebidBannerViewController: GoogleMobileAds.BannerViewDelegate { + + func bannerViewDidReceiveAd(_ bannerView: GoogleMobileAds.BannerView) { + AdViewUtils.findPrebidCreativeSize(bannerView, success: { size in + guard let bannerView = bannerView as? AdManagerBannerView else { return } + bannerView.resize(adSizeFor(cgSize: size)) + }, failure: { error in + print("[PrebidMobile SDK] Error finding creative size: \(error)") + }) + } + + func bannerView( + _ bannerView: GoogleMobileAds.BannerView, + didFailToReceiveAdWithError error: any Error + ) { + print("[GMA SDK] Failed to receive ad: \(error)") + } +} From 8d3063f4f5b090baed42a5e0ced43e01f74d3e1a Mon Sep 17 00:00:00 2001 From: vladislav-yermakov Date: Mon, 22 Dec 2025 18:56:53 +0100 Subject: [PATCH 42/42] - Updated demo-ios-objc with prebid example --- demo-ios-objc/Podfile | 7 +- demo-ios-objc/Podfile.lock | 8 +- .../demo-ios-objc.xcodeproj/project.pbxproj | 18 ++- demo-ios-objc/demo-ios-objc/AppDelegate.m | 20 +++- .../demo-ios-objc/Base.lproj/Main.storyboard | 111 ++++++++++++++++- .../demo-ios-objc/GAMBannerViewController.m | 18 ++- .../demo-ios-objc/OptableSDKDelegate.h | 10 +- .../demo-ios-objc/OptableSDKDelegate.m | 29 ++++- .../PrebidBannerViewController.h | 22 ++++ .../PrebidBannerViewController.m | 112 ++++++++++++++++++ 10 files changed, 336 insertions(+), 19 deletions(-) create mode 100644 demo-ios-objc/demo-ios-objc/PrebidBannerViewController.h create mode 100644 demo-ios-objc/demo-ios-objc/PrebidBannerViewController.m diff --git a/demo-ios-objc/Podfile b/demo-ios-objc/Podfile index c1a71cc..db4bb9b 100644 --- a/demo-ios-objc/Podfile +++ b/demo-ios-objc/Podfile @@ -1,9 +1,5 @@ platform :ios, '15.0' -source 'https://cdn.cocoapods.org/' - -project 'demo-ios-objc.xcodeproj' - target 'demo-ios-objc' do use_frameworks! @@ -12,5 +8,6 @@ target 'demo-ios-objc' do #pod 'OptableSDK' pod 'Google-Mobile-Ads-SDK' - + pod 'PrebidMobile' + end diff --git a/demo-ios-objc/Podfile.lock b/demo-ios-objc/Podfile.lock index dc3bd83..7831b0a 100644 --- a/demo-ios-objc/Podfile.lock +++ b/demo-ios-objc/Podfile.lock @@ -3,15 +3,20 @@ PODS: - GoogleUserMessagingPlatform (>= 1.1) - GoogleUserMessagingPlatform (3.1.0) - OptableSDK (0.10.0) + - PrebidMobile (3.1.0): + - PrebidMobile/core (= 3.1.0) + - PrebidMobile/core (3.1.0) DEPENDENCIES: - Google-Mobile-Ads-SDK - OptableSDK (from `../`) + - PrebidMobile SPEC REPOS: trunk: - Google-Mobile-Ads-SDK - GoogleUserMessagingPlatform + - PrebidMobile EXTERNAL SOURCES: OptableSDK: @@ -21,7 +26,8 @@ SPEC CHECKSUMS: Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2 GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c OptableSDK: fc5d3852c29fac1881b1d3ab6ea397de71c8cbf1 + PrebidMobile: 046bb6220157c7332dc6c6e19a99397bb481ac3a -PODFILE CHECKSUM: b0f8f6f1358c223af6d51da031c3c1e93278a090 +PODFILE CHECKSUM: 84321d4bbdf19f72ce3dfa6f4cb2f0a9869574ad COCOAPODS: 1.16.2 diff --git a/demo-ios-objc/demo-ios-objc.xcodeproj/project.pbxproj b/demo-ios-objc/demo-ios-objc.xcodeproj/project.pbxproj index deefd94..a174292 100644 --- a/demo-ios-objc/demo-ios-objc.xcodeproj/project.pbxproj +++ b/demo-ios-objc/demo-ios-objc.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 63B5A91A25368252000CA436 /* GAMBannerViewController.h in Sources */ = {isa = PBXBuildFile; fileRef = 63B5A8C52536704F000CA436 /* GAMBannerViewController.h */; }; 63B5A91E2536825A000CA436 /* IdentifyViewController.h in Sources */ = {isa = PBXBuildFile; fileRef = 6320EF022535F92300F76877 /* IdentifyViewController.h */; }; 63B5AAE3253E4047000CA436 /* OptableSDKDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 63B5AAE2253E4047000CA436 /* OptableSDKDelegate.m */; }; + CE9FB6162EF9B9D100277231 /* PrebidBannerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE9FB6152EF9B9D100277231 /* PrebidBannerViewController.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -32,7 +33,7 @@ 6320EEFD2535F92300F76877 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 6320EEFF2535F92300F76877 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; 6320EF002535F92300F76877 /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; - 6320EF022535F92300F76877 /* IdentifyViewController.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; path = IdentifyViewController.h; sourceTree = ""; }; + 6320EF022535F92300F76877 /* IdentifyViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IdentifyViewController.h; sourceTree = ""; }; 6320EF032535F92300F76877 /* IdentifyViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IdentifyViewController.m; sourceTree = ""; }; 6320EF062535F92300F76877 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 6320EF082535F92500F76877 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -40,10 +41,12 @@ 6320EF0D2535F92500F76877 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 6320EF0E2535F92500F76877 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 63B5A8C025366FE8000CA436 /* GAMBannerViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GAMBannerViewController.m; sourceTree = ""; }; - 63B5A8C52536704F000CA436 /* GAMBannerViewController.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; fileEncoding = 4; path = GAMBannerViewController.h; sourceTree = ""; }; + 63B5A8C52536704F000CA436 /* GAMBannerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GAMBannerViewController.h; sourceTree = ""; }; 63B5AAE2253E4047000CA436 /* OptableSDKDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OptableSDKDelegate.m; sourceTree = ""; }; 63B5AAE7253E4067000CA436 /* OptableSDKDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OptableSDKDelegate.h; sourceTree = ""; }; 83DB069E12658B3EA13B8867 /* Pods-demo-ios-objcTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-objcTests.debug.xcconfig"; path = "Target Support Files/Pods-demo-ios-objcTests/Pods-demo-ios-objcTests.debug.xcconfig"; sourceTree = ""; }; + CE9FB6142EF9B9D100277231 /* PrebidBannerViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PrebidBannerViewController.h; sourceTree = ""; }; + CE9FB6152EF9B9D100277231 /* PrebidBannerViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PrebidBannerViewController.m; sourceTree = ""; }; DB5262E52B5F4397F9533E61 /* Pods_demo_ios_objcTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_demo_ios_objcTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E1A7E09DC9C49991F9532BB1 /* Pods-demo-ios-objc.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-objc.release.xcconfig"; path = "Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc.release.xcconfig"; sourceTree = ""; }; E6A988EC956D03DD2D0BFF54 /* Pods-demo-ios-objc.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-objc.debug.xcconfig"; path = "Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc.debug.xcconfig"; sourceTree = ""; }; @@ -102,6 +105,8 @@ 6320EF032535F92300F76877 /* IdentifyViewController.m */, 63B5A8C52536704F000CA436 /* GAMBannerViewController.h */, 63B5A8C025366FE8000CA436 /* GAMBannerViewController.m */, + CE9FB6142EF9B9D100277231 /* PrebidBannerViewController.h */, + CE9FB6152EF9B9D100277231 /* PrebidBannerViewController.m */, 6320EF052535F92300F76877 /* Main.storyboard */, 6320EF082535F92500F76877 /* Assets.xcassets */, 6320EF0A2535F92500F76877 /* LaunchScreen.storyboard */, @@ -223,10 +228,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc-frameworks.sh\"\n"; @@ -240,10 +249,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-objc/Pods-demo-ios-objc-resources.sh\"\n"; @@ -256,6 +269,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CE9FB6162EF9B9D100277231 /* PrebidBannerViewController.m in Sources */, 63B5A91E2536825A000CA436 /* IdentifyViewController.h in Sources */, 6320EF042535F92300F76877 /* IdentifyViewController.m in Sources */, 63B5A91A25368252000CA436 /* GAMBannerViewController.h in Sources */, diff --git a/demo-ios-objc/demo-ios-objc/AppDelegate.m b/demo-ios-objc/demo-ios-objc/AppDelegate.m index b49c0b9..98e711c 100644 --- a/demo-ios-objc/demo-ios-objc/AppDelegate.m +++ b/demo-ios-objc/demo-ios-objc/AppDelegate.m @@ -8,8 +8,12 @@ #import "AppDelegate.h" #import "OptableSDKDelegate.h" + @import OptableSDK; +@import PrebidMobile; +@import GoogleMobileAds; + OptableSDK *OPTABLE = nil; @interface AppDelegate () @@ -18,7 +22,6 @@ @interface AppDelegate () @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - // Override point for customization after application launch. OptableSDKDelegate *delegate = [OptableSDKDelegate new]; @@ -28,9 +31,24 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( OPTABLE = [[OptableSDK alloc] initWithConfig: config]; OPTABLE.delegate = delegate; + [self initPrebidMobile]; + [self initGoogleMobileAds]; + return YES; } +- (void)initPrebidMobile { + Prebid.shared.prebidServerAccountId = @"0689a263-318d-448b-a3d4-b02e8a709d9d"; + + [Prebid initializeSDKWithServerURL: @"https://prebid-server-test-j.prebid.org/openrtb2/auction" + error: nil + : nil]; +} + +- (void)initGoogleMobileAds { + [[GADMobileAds sharedInstance] startWithCompletionHandler:^(GADInitializationStatus * _Nonnull status) {}]; +} + #pragma mark - UISceneSession lifecycle - (UISceneConfiguration *)application:(UIApplication *)application diff --git a/demo-ios-objc/demo-ios-objc/Base.lproj/Main.storyboard b/demo-ios-objc/demo-ios-objc/Base.lproj/Main.storyboard index 464d7c2..c3e01bf 100644 --- a/demo-ios-objc/demo-ios-objc/Base.lproj/Main.storyboard +++ b/demo-ios-objc/demo-ios-objc/Base.lproj/Main.storyboard @@ -204,6 +204,7 @@ + @@ -248,6 +249,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -259,7 +368,7 @@ - + diff --git a/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m b/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m index 359369b..dd6203c 100644 --- a/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m +++ b/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m @@ -20,16 +20,20 @@ @interface GAMBannerViewController () @implementation GAMBannerViewController +- (NSString *)AD_MANAGER_AD_UNIT_ID { + return @"/22081946781/ios-sdk-demo/mobile-leaderboard"; +} + - (void)viewDidLoad { [super viewDidLoad]; self.bannerView = [[GADBannerView alloc] initWithAdSize:GADAdSizeBanner]; - self.bannerView.adUnitID = @"/22081946781/ios-sdk-demo/mobile-leaderboard"; - [self addBannerViewToView:self.bannerView]; + self.bannerView.adUnitID = self.AD_MANAGER_AD_UNIT_ID; self.bannerView.rootViewController = self; + [self addBannerViewToView:self.bannerView]; OptableSDKDelegate *delegate = (OptableSDKDelegate *)OPTABLE.delegate; - delegate.bannerView = self.bannerView; + delegate.adManagerBannerView = self.bannerView; delegate.targetingOutput = self.targetingOutput; } @@ -53,16 +57,16 @@ - (IBAction)loadBannerWithTargetingFromCache:(id)sender { GAMRequest *request = [GAMRequest request]; NSDictionary *keyvals = nil; - [_targetingOutput setText:@"Checking local targeting cache...\n\n"]; + [_targetingOutput setText:@"🗂 Checking local targeting cache...\n\n"]; keyvals = [OPTABLE targetingFromCache]; if (keyvals != nil) { request.customTargeting = keyvals; NSLog(@"[OptableSDK] Cached targeting values found: %@", keyvals); - [_targetingOutput setText:[NSString stringWithFormat:@"%@\nFound cached data: %@\n", [_targetingOutput text], keyvals]]; + [_targetingOutput setText:[NSString stringWithFormat:@"%@\n✅ Found cached data: %@\n", [_targetingOutput text], keyvals]]; } else { - [_targetingOutput setText:[NSString stringWithFormat:@"%@\nCache empty.\n", + [_targetingOutput setText:[NSString stringWithFormat:@"%@\nℹ️ Cache empty.\n", [_targetingOutput text]]]; } @@ -81,6 +85,8 @@ - (IBAction)clearTargetingCache:(id)sender { [OPTABLE targetingClearCache]; } +// MARK: - Helpers + - (void)addBannerViewToView:(UIView *)bannerView { bannerView.translatesAutoresizingMaskIntoConstraints = NO; [self.adPlaceholder addSubview:bannerView]; diff --git a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.h b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.h index ae1a2f1..a97ac54 100644 --- a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.h +++ b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.h @@ -7,11 +7,19 @@ // @import OptableSDK; + +@import PrebidMobile; @import GoogleMobileAds; @interface OptableSDKDelegate: NSObject -@property(atomic, readwrite, strong) GADBannerView *bannerView; +// MARK: - PrebidMobile +@property(atomic, readwrite, weak) BannerAdUnit *pbmBannerAdUnit; + +// MARK: - GoogleMobileAds +@property(atomic, readwrite, weak) GADBannerView *adManagerBannerView; + +// MARK: - Text Output @property(atomic, readwrite, strong) UITextView *identifyOutput; @property(atomic, readwrite, strong) UITextView *targetingOutput; diff --git a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m index 5633774..eee1c4a 100644 --- a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m +++ b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m @@ -50,7 +50,7 @@ - (void)targetingOk:(NSDictionary *)result { // Update the GAM banner view with result targeting keyvalues: GAMRequest *request = [GAMRequest request]; request.customTargeting = result; - [self.bannerView loadRequest:request]; + [self loadBannerWithKeyValues: result]; NSLog(@"[OptableSDK] Success on /targeting API call: %@", result); @@ -62,7 +62,7 @@ - (void)targetingOk:(NSDictionary *)result { - (void)targetingErr:(NSError *)error { // Update the GAM banner view without targeting data: GAMRequest *request = [GAMRequest request]; - [self.bannerView loadRequest:request]; + [self.adManagerBannerView loadRequest:request]; NSLog(@"[OptableSDK] Error on /targeting API call: %@", [error localizedDescription]); @@ -87,4 +87,29 @@ - (void)witnessErr:(NSError *)error { self.targetingOutput.text = output; }); } + +- (void)loadBannerWithKeyValues:(NSDictionary * _Nullable)keyValues { + GAMRequest *request = [GAMRequest request]; + + if (self.pbmBannerAdUnit) { + __weak typeof(self) weakSelf = self; + [self.pbmBannerAdUnit fetchDemandWithAdObject:request completion:^(enum ResultCode status) { + if (status != ResultCodePrebidDemandFetchSuccess) { + NSLog(@"[PrebidMobile SDK] Prebid fetch demand failed: %ld", (long)status); + } + if (keyValues.count > 0) { + NSMutableDictionary *merged = [request.customTargeting mutableCopy] ?: [NSMutableDictionary dictionary]; + [merged addEntriesFromDictionary:keyValues]; + request.customTargeting = merged; + } + [weakSelf.adManagerBannerView loadRequest:request]; + }]; + } else { + if (keyValues.count > 0) { + request.customTargeting = keyValues; + } + [self.adManagerBannerView loadRequest:request]; + } +} + @end diff --git a/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.h b/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.h new file mode 100644 index 0000000..b2ece55 --- /dev/null +++ b/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.h @@ -0,0 +1,22 @@ +// +// PrebidBannerViewController.h +// demo-ios-objc +// +// Copyright © 2020 Optable Technologies Inc. All rights reserved. +// See LICENSE for details. +// + +#import + +@interface PrebidBannerViewController : UIViewController + +@property (weak, nonatomic) IBOutlet UIView *adPlaceholder; + +@property (weak, nonatomic) IBOutlet UIButton *loadBannerButton; +@property (weak, nonatomic) IBOutlet UIButton *cachedBannerButton; +@property (weak, nonatomic) IBOutlet UIButton *clearTargetingCacheButton; +@property (weak, nonatomic) IBOutlet UITextView *targetingOutput; + +- (IBAction)loadBannerWithTargeting:(id)sender; + +@end diff --git a/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.m b/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.m new file mode 100644 index 0000000..901adf5 --- /dev/null +++ b/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.m @@ -0,0 +1,112 @@ +// +// PrebidBannerViewController.m +// demo-ios-objc +// +// Copyright © 2020 Optable Technologies Inc. All rights reserved. +// See LICENSE for details. +// + +#import "OptableSDKDelegate.h" +#import "PrebidBannerViewController.h" +#import "AppDelegate.h" + +@import OptableSDK; +@import GoogleMobileAds; + +@interface PrebidBannerViewController () + +@property(nonatomic, strong) GADBannerView *bannerView; +@property(nonatomic, strong) BannerAdUnit *pbmBannerAdUnit; + +@end + +@implementation PrebidBannerViewController + +- (NSString *)AD_MANAGER_AD_UNIT_ID { + return @"/21808260008/prebid_demo_app_original_api_banner"; +} + +- (NSString *)PREBID_STORED_IMP { + return @"prebid-demo-banner-320-50"; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.bannerView = [[GADBannerView alloc] initWithAdSize:GADAdSizeBanner]; + self.bannerView.adUnitID = self.AD_MANAGER_AD_UNIT_ID; + self.bannerView.rootViewController = self; + [self addBannerViewToView:self.bannerView]; + + self.pbmBannerAdUnit = [[BannerAdUnit alloc] initWithConfigId:self.PREBID_STORED_IMP + size:CGSizeMake(320, 50)]; + + OptableSDKDelegate *delegate = (OptableSDKDelegate *)OPTABLE.delegate; + delegate.adManagerBannerView = self.bannerView; + delegate.pbmBannerAdUnit = self.pbmBannerAdUnit; + delegate.targetingOutput = self.targetingOutput; +} + +- (IBAction)loadBannerWithTargeting:(id)sender { + NSError *error = nil; + + [_targetingOutput setText:@"📡 Calling /targeting API...\n"]; + + [OPTABLE targetingWithIds: NULL error: &error]; + [OPTABLE witnessWithEvent: @"PrebidBannerViewController.loadBannerClicked" + properties: @{ @"example": @"value" } + error: &error]; + [OPTABLE profileWithTraits: @{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES } + id: NULL + neighbors: NULL + error: &error]; +} + +- (IBAction)loadBannerWithTargetingFromCache:(id)sender { + NSError *error = nil; + GAMRequest *request = [GAMRequest request]; + NSDictionary *keyvals = nil; + + [_targetingOutput setText:@"🗂 Checking local targeting cache...\n\n"]; + + keyvals = [OPTABLE targetingFromCache]; + + if (keyvals != nil) { + request.customTargeting = keyvals; + NSLog(@"[OptableSDK] Cached targeting values found: %@", keyvals); + [_targetingOutput setText:[NSString stringWithFormat:@"%@\n✅ Found cached data: %@\n", [_targetingOutput text], keyvals]]; + } else { + [_targetingOutput setText:[NSString stringWithFormat:@"%@\nℹ️ Cache empty.\n", + [_targetingOutput text]]]; + } + + [self.bannerView loadRequest:request]; + + + [OPTABLE witnessWithEvent: @"PrebidBannerViewController.loadBannerClicked" + properties: @{ @"example": @"value" } + error: &error]; + [OPTABLE profileWithTraits: @{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES } + id: NULL + neighbors: NULL + error: &error]; +} + +- (IBAction)clearTargetingCache:(id)sender { + [_targetingOutput setText:@"🧹 Clearing local targeting cache.\n"]; + [OPTABLE targetingClearCache]; +} + +// MARK: - Helpers + +- (void)addBannerViewToView:(UIView *)bannerView { + bannerView.translatesAutoresizingMaskIntoConstraints = NO; + [self.adPlaceholder addSubview:bannerView]; + + [NSLayoutConstraint activateConstraints:@[ + [bannerView.centerXAnchor constraintEqualToAnchor:self.adPlaceholder.centerXAnchor], + [bannerView.centerYAnchor constraintEqualToAnchor:self.adPlaceholder.centerYAnchor] + ]]; +} + +@end