diff --git a/.gitignore b/.gitignore
index 57bbf06..648ed38 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ demo-ios-objc.xcworkspace
fastlane/README.md
fastlane/report.xml
fastlane/test_output/
+Source/.DS_Store
\ No newline at end of file
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/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
diff --git a/OptableSDK.podspec b/OptableSDK.podspec
index 1cd4267..3c6f246 100644
--- a/OptableSDK.podspec
+++ b/OptableSDK.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "OptableSDK"
- spec.version = "0.10.0"
+ spec.version = "1.0.0"
spec.summary = "A lightweight SDK used to integrate iOS apps with the Optable Sandbox"
spec.description = <<-DESC
The Optable SDK is used to integrate an iOS application with an instance of the
@@ -14,7 +14,7 @@ Pod::Spec.new do |spec|
spec.author = { "Optable Technologies Inc" => "support@optable.co" }
spec.platform = :ios
- spec.ios.deployment_target = "9.2"
+ spec.ios.deployment_target = "12.0"
spec.swift_version = "5.0"
spec.source = { :git => "https://github.com/Optable/optable-ios-sdk.git", :tag => "#{spec.version}" }
diff --git a/OptableSDK.xcodeproj/project.pbxproj b/OptableSDK.xcodeproj/project.pbxproj
index 05287e6..ac5d134 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,40 @@
/* 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 = (
+ Integration/OptableSDKTests.swift,
+ Misc/cartesianProduct.swift,
+ Misc/Constants.swift,
+ Unit/EdgeAPITests.swift,
+ Unit/OptableIdentifierEncoderTests.swift,
+ Unit/OptableIdentifiersTests.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 +77,8 @@
6352AAF124EAD402002E66EB = {
isa = PBXGroup;
children = (
- 6352AAFD24EAD403002E66EB /* Source */,
- 6352AB0824EAD403002E66EB /* Tests */,
+ CEBE03802EF03AD00027D67F /* Source */,
+ CEBE038B2EF03ADD0027D67F /* Tests */,
6352AAFC24EAD403002E66EB /* Products */,
);
sourceTree = "";
@@ -84,48 +92,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 +99,6 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
- 6352AB0C24EAD403002E66EB /* OptableSDK.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -153,6 +118,9 @@
);
dependencies = (
);
+ fileSystemSynchronizedGroups = (
+ CEBE03802EF03AD00027D67F /* Source */,
+ );
name = OptableSDK;
productName = OptableSDK;
productReference = 6352AAFB24EAD403002E66EB /* OptableSDK.framework */;
@@ -237,14 +205,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 +212,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 6352AB0A24EAD403002E66EB /* OptableSDKTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -415,7 +374,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
- MARKETING_VERSION = 0.10.0;
+ MARKETING_VERSION = 1.0.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
PRODUCT_BUNDLE_IDENTIFIER = co.optable.OptableSDK;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -447,7 +406,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
- MARKETING_VERSION = 0.10.0;
+ MARKETING_VERSION = 1.0.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
PRODUCT_BUNDLE_IDENTIFIER = co.optable.OptableSDK;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -465,6 +424,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",
@@ -485,6 +445,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/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/Package.swift b/Package.swift
index da773f7..725a98d 100644
--- a/Package.swift
+++ b/Package.swift
@@ -5,7 +5,7 @@ import PackageDescription
let package = Package(
name: "OptableSDK",
- platforms: [.iOS(.v9)],
+ platforms: [.iOS(.v12)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
@@ -23,7 +23,7 @@ let package = Package(
name: "OptableSDK",
dependencies: [],
path: "Source",
- exclude: ["Source/Info.plist"]),
+ exclude: ["Info.plist"]),
.testTarget(
name: "OptableSDKTests",
dependencies: ["OptableSDK"],
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 [](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/Source/.DS_Store b/Source/.DS_Store
deleted file mode 100644
index 5008ddf..0000000
Binary files a/Source/.DS_Store and /dev/null differ
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
deleted file mode 100644
index 358c7f4..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: Config) {
- self.storage = LocalStorage(config)
- if (config.useragent == nil) {
- self.userAgent { (realUserAgent) in
- self.ua = realUserAgent
- }
- } else {
- self.ua = config.useragent
- }
- }
-
- 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..b05b572
--- /dev/null
+++ b/Source/Core/EdgeAPI.swift
@@ -0,0 +1,246 @@
+//
+// EdgeAPI.swift
+// OptableSDK
+//
+// Copyright Β© 2026 Optable Technologies, Inc. All rights reserved.
+//
+
+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"
+
+ var storage: LocalStorage
+ var config: OptableConfig
+
+ var userAgent: String?
+
+ private lazy var jsonEncoder = JSONEncoder()
+
+ 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
+ }
+ }
+
+ // MARK: Endpoints
+ func identify(ids: OptableIdentifiers) throws -> URLRequest? {
+ guard let url = buildEdgeAPIURL(endpoint: "identify") else { return nil }
+ let jsonData = try jsonEncoder.encode(ids)
+ let request = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: jsonData)
+ return request
+ }
+
+ func profile(traits: NSDictionary, id: String? = nil, neighbors: [String]? = nil) throws -> URLRequest? {
+ guard let url = buildEdgeAPIURL(endpoint: "profile") else { return nil }
+
+ 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(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) throws -> URLRequest? {
+ guard let url = buildEdgeAPIURL(endpoint: "witness") else { return nil }
+ let request = try buildRequest(.POST, url: url, headers: resolveHeaders(), obj: ["event": event, "properties": properties])
+ return request
+ }
+}
+
+// MARK: - Dispatch
+extension EdgeAPI {
+ 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
+ }
+ 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)
+ }
+ }
+}
+
+// MARK: - Private
+extension EdgeAPI {
+ 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)
+ }
+ }
+
+ func resolveHeaders() -> HTTPHeaders {
+ var headers = HTTPHeaders()
+ headers[.accept] = "application/json"
+ headers[.contentType] = "application/json"
+
+ if let userAgent {
+ headers[.userAgent] = userAgent
+ }
+
+ if let apiKey = config.apiKey {
+ headers[.authorization] = "Bearer \(apiKey)"
+ }
+
+ if let passport: String = storage.getPassport() {
+ headers[kPassportHeader] = passport
+ }
+
+ return headers
+ }
+
+ 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 obj = obj {
+ let reqBodyJSON = try JSONSerialization.data(withJSONObject: obj, options: [])
+ request.httpBody = reqBodyJSON
+ }
+
+ for (key, value) in headers.asDict {
+ request.addValue(value, forHTTPHeaderField: key)
+ }
+
+ 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
+ }
+
+ 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)),
+ ]
+
+ 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/LocalStorage.swift b/Source/Core/LocalStorage.swift
index 6ae2729..8c412f2 100644
--- a/Source/Core/LocalStorage.swift
+++ b/Source/Core/LocalStorage.swift
@@ -2,45 +2,60 @@
// 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
-class LocalStorage: NSObject {
+/**
+ 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
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)
}
- func getTargeting() -> [String: Any]? {
- return UserDefaults.standard.dictionary(forKey: targetingKey)
+ func getTargeting() -> OptableTargeting? {
+ if let targetingData = UserDefaults.standard.data(forKey: targetingKey),
+ let targeting = try? NSKeyedUnarchiver.unarchivedObject(
+ ofClass: OptableTargeting.self,
+ from: targetingData
+ ) {
+ return targeting
+ }
+
+ return nil
}
- func setTargeting(_ keyvalues: [String: Any]) -> Void {
- UserDefaults.standard.setValue(keyvalues, forKey: targetingKey)
+ func setTargeting(_ targeting: OptableTargeting) {
+ let targetingData = try? NSKeyedArchiver.archivedData(
+ withRootObject: targeting,
+ requiringSecureCoding: true
+ )
+ UserDefaults.standard.setValue(targetingData, forKey: targetingKey)
}
- func clearTargeting() -> Void {
+ func clearTargeting() {
UserDefaults.standard.removeObject(forKey: targetingKey)
}
}
diff --git a/Source/Core/OptableError.swift b/Source/Core/OptableError.swift
new file mode 100644
index 0000000..96c473b
--- /dev/null
+++ b/Source/Core/OptableError.swift
@@ -0,0 +1,26 @@
+//
+// OptableError.swift
+// OptableSDK
+//
+// Copyright Β© 2026 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..55a0d8d
--- /dev/null
+++ b/Source/Core/OptableIdentifierEncoder.swift
@@ -0,0 +1,183 @@
+//
+// OptableIdentifierEncoder.swift
+// OptableSDK
+//
+// Copyright Β© 2026 Optable Technologies, Inc. All rights reserved.
+//
+
+import CommonCrypto
+import Foundation
+#if canImport(CryptoKit)
+ import CryptoKit
+#endif
+
+// MARK: - OptableIdentifierEncoder
+enum OptableIdentifierEncoder {
+ /// Builds Extended Identifier from Email address
+ static func email(_ email: String) -> String {
+ let prefix = OptableIdentifierType.emailAddress.rawValue
+ let normalizedData = Data(email.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased().utf8)
+ let identifier = sha256(data: normalizedData)
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from Phone number
+ static func phoneNumber(_ phoneNumber: String) -> String {
+ let prefix = OptableIdentifierType.phoneNumber.rawValue
+ let normalizedData = Data(phoneNumber.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased().utf8)
+ let identifier = sha256(data: normalizedData)
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from Postal code
+ static func postalCode(_ postalCode: String) -> String {
+ let prefix = OptableIdentifierType.postalCode.rawValue
+ let identifier = postalCode.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from IPv4 address
+ static func ipv4(_ ipv4: String) -> String {
+ let prefix = OptableIdentifierType.ipv4Address.rawValue
+ let identifier = ipv4.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from IPv6 address
+ static func ipv6(_ ipv6: String) -> String {
+ let prefix = OptableIdentifierType.ipv6Address.rawValue
+ let identifier = ipv6.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from Apple IDFA
+ static func idfa(_ idfa: String) -> String {
+ let prefix = OptableIdentifierType.appleIDFA.rawValue
+ let identifier = idfa.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from Google GAID
+ static func gaid(_ gaid: String) -> String {
+ let prefix = OptableIdentifierType.googleGAID.rawValue
+ let identifier = gaid.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from Roku RIDA
+ static func rida(_ rida: String) -> String {
+ let prefix = OptableIdentifierType.rokuRIDA.rawValue
+ let identifier = rida.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from Samsung TV TIFA
+ static func tifa(_ tifa: String) -> String {
+ let prefix = OptableIdentifierType.samsungTIFA.rawValue
+ let identifier = tifa.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from Amazon Fire AFAI
+ static func afai(_ afai: String) -> String {
+ let prefix = OptableIdentifierType.amazonFireAFAI.rawValue
+ let identifier = afai.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from NetID
+ static func netid(_ netid: String) -> String {
+ let prefix = OptableIdentifierType.netID.rawValue
+ let identifier = netid.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from ID5
+ static func id5(_ id5: String) -> String {
+ let prefix = OptableIdentifierType.id5.rawValue
+ let identifier = id5.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from Utiq
+ static func utiq(_ utiq: String) -> String {
+ let prefix = OptableIdentifierType.utiq.rawValue
+ let identifier = utiq.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from Custom Publisher Provided ID (PPID)
+ static func custom(idx: Int = 0, _ ppid: String) -> String {
+ let prefix = OptableIdentifierType.custom(idx).rawValue
+ let identifier = ppid.trimmingCharacters(in: .whitespacesAndNewlines)
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Extended Identifier from Optable Visitor ID (VID)
+ static func vid(_ vid: String) -> String {
+ let prefix = OptableIdentifierType.optableVID.rawValue
+ let identifier = vid.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined()
+ return "\(prefix):\(identifier)"
+ }
+
+ ///
+ /// 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()
+ ///
+ 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 "" }
+
+ /// 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()
+ }
+
+ // MARK: - Private
+ private static 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 cchash(data)
+ }
+
+ 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)
+ }
+ return digest.makeIterator().compactMap {
+ String(format: "%02x", $0)
+ }.joined()
+ }
+}
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/Misc/AppTrackingTransparency.swift b/Source/Misc/AppTrackingTransparency.swift
new file mode 100644
index 0000000..b2115f3
--- /dev/null
+++ b/Source/Misc/AppTrackingTransparency.swift
@@ -0,0 +1,87 @@
+//
+// AppTrackingTransparency.swift
+// OptableSDK
+//
+// Copyright Β© 2026 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 advertisingIdentifierAvailable: 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
diff --git a/Source/Misc/IABConsent.swift b/Source/Misc/IABConsent.swift
new file mode 100644
index 0000000..b341ffa
--- /dev/null
+++ b/Source/Misc/IABConsent.swift
@@ -0,0 +1,36 @@
+//
+// IABConsent.swift
+// OptableSDK
+//
+// Copyright Β© 2026 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/Misc/Networking.swift b/Source/Misc/Networking.swift
new file mode 100644
index 0000000..47d8cf4
--- /dev/null
+++ b/Source/Misc/Networking.swift
@@ -0,0 +1,177 @@
+//
+// Networking.swift
+// OptableSDK
+//
+// Copyright Β© 2026 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: Int {
+ // 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
+
+ var isInformational: Bool {
+ (100 ..< 200).contains(rawValue)
+ }
+
+ var isSuccess: Bool {
+ (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/Misc/URL+Compat.swift b/Source/Misc/URL+Compat.swift
new file mode 100644
index 0000000..c69720a
--- /dev/null
+++ b/Source/Misc/URL+Compat.swift
@@ -0,0 +1,21 @@
+//
+// URL+Compat.swift
+// OptableSDK
+//
+// Copyright Β© 2026 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/OptableConfig.swift b/Source/OptableConfig.swift
new file mode 100644
index 0000000..4cc71cf
--- /dev/null
+++ b/Source/OptableConfig.swift
@@ -0,0 +1,127 @@
+//
+// OptableConfig.swift
+// OptableSDK
+//
+// Copyright Β© 2020 Optable Technologies Inc. All rights reserved.
+// See LICENSE for details.
+//
+
+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
+
+ /// The DCN's Source Slug. E.g. `acmeco-sdk`.
+ @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"
+
+ /// The API path to be appended to the host. Default value is "v2".
+ @objc
+ public var path: String = "v2"
+
+ /// Boolean flag that determines if insecure HTTP should be used instead of HTTPS. Default is false.
+ @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.
+ @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?
+
+ // MARK: Inits
+ /**
+ - 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,
+ 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
+ }
+}
diff --git a/Source/OptableIdentifierType.swift b/Source/OptableIdentifierType.swift
new file mode 100644
index 0000000..cd031d7
--- /dev/null
+++ b/Source/OptableIdentifierType.swift
@@ -0,0 +1,92 @@
+//
+// OptableIdentifierType.swift
+// OptableSDK
+//
+// Copyright Β© 2026 Optable Technologies, Inc. All rights reserved.
+//
+
+import Foundation
+
+/**
+ Optable Identifier Types
+
+ For more info check:
+ [](https://docs.optable.co/optable-documentation/getting-started/reference/identifier-types)
+
+ */
+public enum OptableIdentifierType: 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 abs(n) == 0 ? "c" : "c\(abs(n))"
+ case .optableVID: return "v"
+ }
+ }
+}
diff --git a/Source/OptableIdentifiers.swift b/Source/OptableIdentifiers.swift
new file mode 100644
index 0000000..2b2a151
--- /dev/null
+++ b/Source/OptableIdentifiers.swift
@@ -0,0 +1,127 @@
+//
+// OptableIdentifiers.swift
+// OptableSDK
+//
+// Copyright Β© 2026 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 }
+ }
+
+ public init(_ dict: [OptableIdentifierType: String]) {
+ self.dict = Dictionary(uniqueKeysWithValues: dict.map({ ($0.key.rawValue, $0.value) }))
+ }
+
+ public subscript(_ key: OptableIdentifierType) -> String? {
+ 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] = []
+
+ for (key, value) in dict {
+ guard
+ value.isEmpty == false, // skip empty values
+ 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 extendedIds = generateExtendedIds()
+
+ var container = encoder.unkeyedContainer()
+ for eid in extendedIds {
+ try container.encode(eid)
+ }
+ }
+}
diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift
index be2ec69..d5f9c58 100644
--- a/Source/OptableSDK.swift
+++ b/Source/OptableSDK.swift
@@ -7,183 +7,349 @@
//
import Foundation
-import CommonCrypto
-#if canImport(CryptoKit)
-import CryptoKit
-#endif
-import AppTrackingTransparency
-import AdSupport
-
-///
-/// 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.
-///
+
+// 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.
+ */
@objc
public protocol OptableDelegate {
func identifyOk(_ result: HTTPURLResponse)
func identifyErr(_ error: NSError)
func profileOk(_ result: HTTPURLResponse)
func profileErr(_ error: NSError)
- func targetingOk(_ result: NSDictionary)
+ func targetingOk(_ result: OptableTargeting)
func targetingErr(_ error: NSError)
func witnessOk(_ result: HTTPURLResponse)
func witnessErr(_ error: NSError)
}
-///
-/// 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.
-///
+// 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.
+ */
@objc
public class OptableSDK: NSObject {
- @objc public var delegate: OptableDelegate?
+ @objc
+ public var delegate: OptableDelegate?
+
+ let config: OptableConfig
+ let api: EdgeAPI
- public enum OptableError: Error {
- case identify(String)
- case profile(String)
- case targeting(String)
- case witness(String)
+ /// `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)
+
+ // Automatically request Tracking Authorization
+ if #available(iOS 14, *) {
+ if config.skipAdvertisingIdDetection == false, ATT.canAuthorize {
+ ATT.requestATTAuthorization()
+ }
+ }
}
- var config: Config
- var client: Client
+ /// OptableSDK version
+ static var version: String {
+ let sdkBundle = Bundle(for: OptableSDK.self)
- ///
- /// OptableSDK(host, app) returns an instance of the SDK configured to talk to the sandbox specified by host & app:
- ///
- @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)
+ 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: "-")
}
+}
- ///
- /// 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 -> Void {
- try 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)")))
- } else {
- completion(.failure(OptableError.identify("Session error: Unknown")))
- }
- 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.identify(msg)))
- return
+// 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(.init(emailAddress: "example@example.com", phoneNumber: "1234567890"), completion)
+ ```
+ */
+ func identify(_ ids: OptableIdentifiers, _ completion: @escaping (Result) -> Void) throws {
+ try _identify(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: OptableIdentifiers) async throws -> HTTPURLResponse {
+ return try await withCheckedThrowingContinuation({ [unowned self] continuation in
+ do {
+ try self._identify(ids, completion: { continuation.resume(with: $0) })
+ } catch {
+ continuation.resume(throwing: error)
}
- completion(.success(response))
- }?.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.
- ///
+ // 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
- public func identify(_ ids: [String]) throws -> Void {
- try self.identify(ids: ids) { result in
+ func identify(_ ids: [String: String]) throws {
+ try self._identify(OptableIdentifiers(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)
}
}
}
+}
- ///
- /// 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.
- ///
- public func identify(email: String, aaid: Bool = false, ppid: String = "", _ completion: @escaping (Result) -> Void) throws -> Void {
- var ids = [String]()
+// 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.
- if (email != "") {
- ids.append(self.eid(email))
- }
+ 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.
- 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))
- }
+ 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(ids: [String]? = nil, completion: @escaping (Result) -> Void) throws {
+ try _targeting(ids: ids, completion: completion)
+ }
+
+ /// targetingFromCache() returns the previously cached targeting data, if any.
+ @objc
+ func targetingFromCache() -> OptableTargeting? {
+ return self.api.storage.getTargeting()
+ }
+
+ /// 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(ids: [String]? = nil) async throws -> OptableTargeting {
+ return try await withCheckedThrowingContinuation({ [unowned self] continuation in
+ do {
+ try self._targeting(ids: ids, completion: { continuation.resume(with: $0) })
+ } catch {
+ continuation.resume(throwing: error)
}
- }
+ })
+ }
- if ppid.count > 0 {
- ids.append(self.cid(ppid))
- }
+ // 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(ids: [String]? = nil) throws {
+ try self._targeting(ids: ids, completion: { result in
+ switch result {
+ case let .success(optableTargeting):
+ self.delegate?.targetingOk(optableTargeting)
+ 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.
- try self.identify(ids: ids, completion)
+ 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)
}
- ///
- /// 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.
- ///
+ // 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
- public func identify(_ email: String, aaid: Bool = false, ppid: String = "") throws -> Void {
- try self.identify(email: email, aaid: aaid, ppid: ppid) { result in
+ func witness(event: String, properties: NSDictionary) throws {
+ try self.witness(event: event, properties: properties) { result in
switch result {
- case .success(let response):
- self.delegate?.identifyOk(response)
- case .failure(let error as NSError):
- self.delegate?.identifyErr(error)
+ case let .success(response):
+ self.delegate?.witnessOk(response)
+ case let .failure(error as NSError):
+ self.delegate?.witnessErr(error)
}
}
}
+}
+// MARK: - Profile
+public extension OptableSDK {
+ /**
+ 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, 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
+ /**
+ 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, id: String? = nil, neighbors: [String]? = nil) async throws -> HTTPURLResponse {
+ return try await withCheckedThrowingContinuation({ [unowned self] continuation in
+ do {
+ try self._profile(traits: traits, id: id, neighbors: neighbors, 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, 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)
+ case let .failure(error as NSError):
+ self.delegate?.profileErr(error)
+ }
+ })
+ }
+}
+
+// MARK: - Identify from URL
+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 OptableError on failure.
+ /// 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]).
///
- /// On success, this method will also cache the resulting targeting data in client storage, which can
- /// be access using targetingFromCache(), and cleared using targetingClearCache().
+ /// 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.
///
- public func targeting(_ completion: @escaping (Result) -> Void) throws -> Void {
- try Targeting(config: self.config, client: self.client) { (data, response, error) in
+ @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 */ })
+ }
+}
+
+// MARK: - Private
+private extension OptableSDK {
+ func _identify(_ ids: OptableIdentifiers, completion: @escaping (Result) -> Void) throws {
+ var ids = ids
+
+ if config.skipAdvertisingIdDetection == false,
+ ATT.advertisingIdentifierAvailable,
+ ATT.advertisingIdentifier != UUID(uuid: uuid_t(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)),
+ ids[.appleIDFA] != nil {
+ ids[.appleIDFA] = ATT.advertisingIdentifier.uuidString
+ }
+
+ 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)")))
+ } else {
+ completion(.failure(OptableError.identify("Session error: Unknown")))
+ }
+ return
+ }
+ 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
+ }
+ completion(.success(response))
+ }).resume()
+ }
+
+ 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)")))
@@ -192,77 +358,40 @@ public class OptableSDK: NSObject {
}
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 HTTPStatusCode(rawValue: response.statusCode)?.isSuccess == true else {
+ let errDesc = OptableSDK.generateEdgeAPIErrorDescription(with: data, response: response)
+ completion(.failure(OptableError.targeting(errDesc, code: response.statusCode)))
return
}
do {
- let keyvalues = try JSONSerialization.jsonObject(with: data ?? Data(), options: [])
- let result = keyvalues as? NSDictionary ?? NSDictionary()
+ let optableTargetingData = try JSONSerialization.jsonObject(with: data ?? Data(), options: [])
+ let optableTargetingDict: NSMutableDictionary = (
+ (optableTargetingData as? NSDictionary)?.mutableCopy() as? NSMutableDictionary
+ ) ?? NSMutableDictionary()
+
+ let optableTargeting = OptableTargeting(
+ optableTargeting: optableTargetingDict,
+ gamTargetingKeywords: OptableSDK.generateGAMTargetingKeywords(from: optableTargetingDict),
+ ortb2: OptableSDK.generateORTB2Config(from: optableTargetingDict)
+ )
/// 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(optableTargeting)
- completion(.success(result))
+ completion(.success(optableTargeting))
} catch {
completion(.failure(OptableError.targeting("Error parsing JSON response: \(error)")))
}
- }?.resume()
+ }).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 -> Void {
- try self.targeting() { result in
- switch result {
- case .success(let keyvalues):
- self.delegate?.targetingOk(keyvalues)
- case .failure(let 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 {
- return nil
+ func _witness(event: String, properties: NSDictionary, completion: @escaping (Result) -> Void) throws {
+ guard let request = try api.witness(event: event, properties: properties) else {
+ throw OptableError.witness("Failed to create witness request")
}
- return keyvalues
- }
- ///
- /// targetingClearCache() clears any previously cached targeting data.
- ///
- @objc
- public func targetingClearCache() -> Void {
- self.client.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.
- ///
- 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
+ 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)")))
@@ -271,47 +400,21 @@ public class OptableSDK: NSObject {
}
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 HTTPStatusCode(rawValue: response.statusCode)?.isSuccess == true else {
+ let errDesc = OptableSDK.generateEdgeAPIErrorDescription(with: data, response: response)
+ completion(.failure(OptableError.witness(errDesc, code: response.statusCode)))
return
}
completion(.success(response))
- }?.resume()
+ }).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 -> Void {
- try self.witness(event: event, properties: properties) { result in
- switch result {
- case .success(let response):
- self.delegate?.witnessOk(response)
- case .failure(let error as NSError):
- self.delegate?.witnessErr(error)
- }
+ 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")
}
- }
- ///
- /// 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.
- ///
- 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
+ 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)")))
@@ -320,150 +423,50 @@ public class OptableSDK: NSObject {
}
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 HTTPStatusCode(rawValue: response.statusCode)?.isSuccess == true else {
+ let errDesc = OptableSDK.generateEdgeAPIErrorDescription(with: data, response: response)
+ completion(.failure(OptableError.profile(errDesc, code: response.statusCode)))
return
}
completion(.success(response))
- }?.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 -> Void {
- try self.profile(traits: traits) { result in
- switch result {
- case .success(let response):
- self.delegate?.profileOk(response)
- case .failure(let error as NSError):
- self.delegate?.profileErr(error)
- }
- }
+ }).resume()
}
- ///
- /// eid(email) is a helper that returns type-prefixed SHA256(downcase(email))
- ///
- @objc
- public 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
+ 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
}
- @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()
- }
+ static func generateGAMTargetingKeywords(from targetingData: NSDictionary?) -> NSDictionary? {
+ guard
+ let targetingData,
+ (targetingData as Dictionary).isEmpty == false,
+ let audienceData = targetingData["audience"] as? [NSDictionary]
+ else { return nil }
- ///
- /// aaid(idfa) is a helper that returns the type-prefixed Apple ID For Advertising
- ///
- @objc
- public func aaid(_ idfa: String) -> String {
- return "a:" + idfa.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
- }
+ let gamTargetingKeywords = NSMutableDictionary()
- ///
- /// cid(ppid) is a helper that returns custom type-prefixed origin-provided PPID
- ///
- @objc
- public 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
- public 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
+ for audience in audienceData {
+ if let keyspace = audience["keyspace"] as? String, let ids = audience["ids"] as? [[String: String]] {
+ gamTargetingKeywords[keyspace] = ids.compactMap(\.values.first).joined(separator: ",")
}
- 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()
+ return gamTargetingKeywords
}
- ///
- /// 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
- public func tryIdentifyFromURL(_ urlString: String) throws -> Void {
- let oeid = self.eidFromURL(urlString)
-
- if (oeid.count > 0) {
- try self.identify(ids: [oeid]) { _ in /* no-op */ }
- }
- }
-
- ///
- /// 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.
- ///
- public static var version: String {
- guard let version = Bundle(for: OptableSDK.self).infoDictionary?["CFBundleShortVersionString"] as? String else {
- return "ios-unknown"
- }
- return "ios-" + version
+ static func generateORTB2Config(from targetingData: NSDictionary?) -> String? {
+ guard
+ let targetingData,
+ (targetingData as Dictionary).isEmpty == false,
+ let ortbConfig = targetingData["ortb2"] as? NSDictionary,
+ let ortbJSONData = try? JSONSerialization.data(withJSONObject: ortbConfig, options: []),
+ let ortbJSONString = String(data: ortbJSONData, encoding: .utf8)
+ else { return nil }
+ return ortbJSONString
}
}
diff --git a/Source/OptableTargeting.swift b/Source/OptableTargeting.swift
new file mode 100644
index 0000000..07444a5
--- /dev/null
+++ b/Source/OptableTargeting.swift
@@ -0,0 +1,48 @@
+//
+// OptableTargeting.swift
+// OptableSDK
+//
+// Copyright Β© 2026 Optable Technologies, Inc. All rights reserved.
+//
+
+import Foundation
+
+@objcMembers
+public class OptableTargeting: NSObject, NSSecureCoding {
+ public static var supportsSecureCoding = true
+
+ public let targetingData: NSDictionary
+ public let gamTargetingKeywords: NSDictionary?
+ public let ortb2: String?
+
+ public func encode(with coder: NSCoder) {
+ coder.encode(targetingData, forKey: "targetingData")
+ coder.encode(gamTargetingKeywords, forKey: "gamTargetingKeywords")
+ coder.encode(ortb2, forKey: "ortb2")
+ }
+
+ public required init?(coder: NSCoder) {
+ targetingData = coder.decodeObject(of: NSDictionary.self, forKey: "targetingData") ?? [:]
+ gamTargetingKeywords = coder.decodeObject(of: NSDictionary.self, forKey: "gamTargetingKeywords")
+ ortb2 = coder.decodeObject(of: NSString.self, forKey: "ortb2") as String?
+ }
+
+ public init(optableTargeting: NSDictionary, gamTargetingKeywords: NSDictionary? = nil, ortb2: String? = nil) {
+ self.targetingData = optableTargeting
+ self.gamTargetingKeywords = gamTargetingKeywords
+ self.ortb2 = ortb2
+ }
+
+ override public var debugDescription: String {
+ var desc = ""
+ return desc
+ }
+}
diff --git a/Tests/Integration/OptableSDKTests.swift b/Tests/Integration/OptableSDKTests.swift
new file mode 100644
index 0000000..b50f2e5
--- /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: 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")
+ 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 = try await sdk.targeting()
+ XCTAssert(response.targetingData.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.targetingData.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: OptableTargeting) {
+ XCTAssert(result.targetingData.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/Misc/Constants.swift b/Tests/Misc/Constants.swift
new file mode 100644
index 0000000..6e51fae
--- /dev/null
+++ b/Tests/Misc/Constants.swift
@@ -0,0 +1,54 @@
+//
+// Constants.swift
+// OptableSDK
+//
+// Copyright Β© 2026 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/Misc/cartesianProduct.swift b/Tests/Misc/cartesianProduct.swift
new file mode 100644
index 0000000..96659e3
--- /dev/null
+++ b/Tests/Misc/cartesianProduct.swift
@@ -0,0 +1,21 @@
+//
+// XCTAssertEqual.swift
+// OptableSDK
+//
+// Copyright Β© 2026 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]
+ }
+ }
+ }
+}
diff --git a/Tests/OptableSDKTests.swift b/Tests/OptableSDKTests.swift
deleted file mode 100644
index f6d2677..0000000
--- a/Tests/OptableSDKTests.swift
+++ /dev/null
@@ -1,116 +0,0 @@
-//
-// OptableSDKTests.swift
-// OptableSDKTests
-//
-// Copyright Β© 2020 Optable Technologies Inc. All rights reserved.
-// See LICENSE for details.
-//
-
-import XCTest
-@testable import OptableSDK
-
-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)
-
- 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.!!!"))
- }
-
- 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.
- }
- }
-
-}
diff --git a/Tests/Unit/EdgeAPITests.swift b/Tests/Unit/EdgeAPITests.swift
new file mode 100644
index 0000000..52531bf
--- /dev/null
+++ b/Tests/Unit/EdgeAPITests.swift
@@ -0,0 +1,242 @@
+//
+// EdgeAPITests.swift
+// OptableSDK
+//
+// Copyright Β© 2026 Optable Technologies, Inc. All rights reserved.
+//
+
+@testable import OptableSDK
+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}}`
+
+ For more info check:
+ [](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide)
+ */
+ 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
+ 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)")
+ 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)
+ })
+ }
+
+ /**
+ 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")
+ }
+
+ // 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 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")
+ }
+ }
+}
diff --git a/Tests/Unit/OptableIdentifierEncoderTests.swift b/Tests/Unit/OptableIdentifierEncoderTests.swift
new file mode 100644
index 0000000..f152926
--- /dev/null
+++ b/Tests/Unit/OptableIdentifierEncoderTests.swift
@@ -0,0 +1,159 @@
+//
+// OptableIdentifierEncoderTests.swift
+// OptableSDK
+//
+// Copyright Β© 2026 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:m5v 3l9", SUT.postalCode(" M5V 3L9"))
+ XCTAssertEqual("z:t 2 p 5 h 1", SUT.postalCode("T 2 P 5 H 1"))
+ XCTAssertEqual("z:90210", SUT.postalCode("90210"))
+ XCTAssertEqual("z:10001", SUT.postalCode("10001"))
+ XCTAssertEqual("z:sw1a 1aa", SUT.postalCode("SW1A 1AA"))
+ XCTAssertEqual("z:eh1 1bb", 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/Unit/OptableIdentifiersTests.swift b/Tests/Unit/OptableIdentifiersTests.swift
new file mode 100644
index 0000000..7017d6a
--- /dev/null
+++ b/Tests/Unit/OptableIdentifiersTests.swift
@@ -0,0 +1,131 @@
+//
+// OptableIdentifiersTests.swift
+// OptableSDK
+//
+// Copyright Β© 2026 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_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",
+ "c2": "",
+ ]
+ )
+ 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",
+ "c2": "",
+ ])
+ 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([
+ .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(nil): "d29c551097b9dd0b82423827f65161232efaf7fc",
+ .custom(1): "AaaZza.dh012",
+ .custom(2): "",
+ ])
+ 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:m5v 3l9" }))
+ 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" }))
+ // Empty should be ignored
+ XCTAssertFalse(decodedData.contains(where: { $0.contains("c2:") }))
+ }
+}
diff --git a/demo-ios-objc/Podfile b/demo-ios-objc/Podfile
index 71f6808..db4bb9b 100644
--- a/demo-ios-objc/Podfile
+++ b/demo-ios-objc/Podfile
@@ -1,8 +1,4 @@
-platform :ios, '14.0'
-
-source 'https://cdn.cocoapods.org/'
-
-project 'demo-ios-objc.xcodeproj'
+platform :ios, '15.0'
target 'demo-ios-objc' do
use_frameworks!
@@ -12,14 +8,6 @@ target 'demo-ios-objc' do
#pod 'OptableSDK'
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
-
+ pod 'PrebidMobile'
+
end
diff --git a/demo-ios-objc/Podfile.lock b/demo-ios-objc/Podfile.lock
index 2fb5c4b..8ea861c 100644
--- a/demo-ios-objc/Podfile.lock
+++ b/demo-ios-objc/Podfile.lock
@@ -1,27 +1,33 @@
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)
+ - 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:
:path: "../"
SPEC CHECKSUMS:
- Google-Mobile-Ads-SDK: 4dde70a8c18d96b14f9548759b8cec6ecb0bc3e6
- GoogleUserMessagingPlatform: f8d0cdad3ca835406755d0a69aa634f00e76d576
- OptableSDK: fc5d3852c29fac1881b1d3ab6ea397de71c8cbf1
+ Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2
+ GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c
+ OptableSDK: ce87b57c73d9324438f555a7e2b1b6da2c33069a
+ PrebidMobile: 046bb6220157c7332dc6c6e19a99397bb481ac3a
-PODFILE CHECKSUM: 48d927338b39550c29272b694f6c18710f33f913
+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 c53bf04..7773004 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,14 @@
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 */; };
+ CE144EE22F11540200C787D8 /* HTTPURLLogProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE144EE12F11540200C787D8 /* HTTPURLLogProtocol.swift */; };
+ CE9FB6162EF9B9D100277231 /* PrebidBannerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE9FB6152EF9B9D100277231 /* PrebidBannerViewController.m */; };
/* 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; };
@@ -53,24 +34,22 @@
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 = ""; };
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 = ""; };
+ 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 = ""; };
+ CE144EE12F11540200C787D8 /* HTTPURLLogProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLLogProtocol.swift; sourceTree = ""; };
+ CE144EED2F12708F00C787D8 /* demo-ios-objc-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "demo-ios-objc-Bridging-Header.h"; 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 = ""; };
@@ -85,22 +64,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 +81,6 @@
isa = PBXGroup;
children = (
6320EEFB2535F92300F76877 /* demo-ios-objc */,
- 6320EF172535F92600F76877 /* demo-ios-objcTests */,
- 6320EF222535F92600F76877 /* demo-ios-objcUITests */,
6320EEFA2535F92300F76877 /* Products */,
689016CDEAF48669106E413E /* Pods */,
2CA740F147AE15FDA936899B /* Frameworks */,
@@ -130,8 +91,6 @@
isa = PBXGroup;
children = (
6320EEF92535F92300F76877 /* demo-ios-objc.app */,
- 6320EF142535F92600F76877 /* demo-ios-objcTests.xctest */,
- 6320EF1F2535F92600F76877 /* demo-ios-objcUITests.xctest */,
);
name = Products;
sourceTree = "";
@@ -139,6 +98,7 @@
6320EEFB2535F92300F76877 /* demo-ios-objc */ = {
isa = PBXGroup;
children = (
+ CE144EED2F12708F00C787D8 /* demo-ios-objc-Bridging-Header.h */,
6320EEFC2535F92300F76877 /* AppDelegate.h */,
6320EEFD2535F92300F76877 /* AppDelegate.m */,
63B5AAE7253E4067000CA436 /* OptableSDKDelegate.h */,
@@ -149,33 +109,18 @@
6320EF032535F92300F76877 /* IdentifyViewController.m */,
63B5A8C52536704F000CA436 /* GAMBannerViewController.h */,
63B5A8C025366FE8000CA436 /* GAMBannerViewController.m */,
+ CE9FB6142EF9B9D100277231 /* PrebidBannerViewController.h */,
+ CE9FB6152EF9B9D100277231 /* PrebidBannerViewController.m */,
6320EF052535F92300F76877 /* Main.storyboard */,
6320EF082535F92500F76877 /* Assets.xcassets */,
6320EF0A2535F92500F76877 /* LaunchScreen.storyboard */,
6320EF0D2535F92500F76877 /* Info.plist */,
6320EF0E2535F92500F76877 /* main.m */,
+ CE144EE02F1153FF00C787D8 /* Misc */,
);
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 = (
@@ -189,6 +134,14 @@
path = Pods;
sourceTree = "";
};
+ CE144EE02F1153FF00C787D8 /* Misc */ = {
+ isa = PBXGroup;
+ children = (
+ CE144EE12F11540200C787D8 /* HTTPURLLogProtocol.swift */,
+ );
+ path = Misc;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -212,46 +165,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 +177,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 +193,6 @@
projectRoot = "";
targets = (
6320EEF82535F92300F76877 /* demo-ios-objc */,
- 6320EF132535F92600F76877 /* demo-ios-objcTests */,
- 6320EF1E2535F92600F76877 /* demo-ios-objcUITests */,
);
};
/* End PBXProject section */
@@ -305,20 +208,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 */
@@ -365,50 +254,6 @@
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;
@@ -430,48 +275,6 @@
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 */
@@ -479,6 +282,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ CE144EE22F11540200C787D8 /* HTTPURLLogProtocol.swift in Sources */,
+ CE9FB6162EF9B9D100277231 /* PrebidBannerViewController.m in Sources */,
63B5A91E2536825A000CA436 /* IdentifyViewController.h in Sources */,
6320EF042535F92300F76877 /* IdentifyViewController.m in Sources */,
63B5A91A25368252000CA436 /* GAMBannerViewController.h in Sources */,
@@ -490,37 +295,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;
@@ -670,7 +446,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-objc";
PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "demo-ios-objc/demo-ios-objc-Bridging-Header.h";
+ SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/demo-ios-objc/demo-ios-objc-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -694,94 +470,12 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-objc";
PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "demo-ios-objc/demo-ios-objc-Bridging-Header.h";
+ SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/demo-ios-objc/demo-ios-objc-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
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 +497,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..c3644d8 100644
--- a/demo-ios-objc/demo-ios-objc/AppDelegate.m
+++ b/demo-ios-objc/demo-ios-objc/AppDelegate.m
@@ -8,7 +8,12 @@
#import "AppDelegate.h"
#import "OptableSDKDelegate.h"
+#import "demo_ios_objc-Swift.h"
+
@import OptableSDK;
+@import PrebidMobile;
+@import GoogleMobileAds;
+
OptableSDK *OPTABLE = nil;
@@ -18,15 +23,36 @@ @interface AppDelegate ()
@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];
+ // Debug URLSession
+ [NSURLProtocol registerClass: [HTTPURLLogProtocol class]];
+
+ 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;
+
+ [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..e4b95bd 100644
--- a/demo-ios-objc/demo-ios-objc/Base.lproj/Main.storyboard
+++ b/demo-ios-objc/demo-ios-objc/Base.lproj/Main.storyboard
@@ -1,9 +1,9 @@
-
+
-
+
@@ -18,7 +18,7 @@
-
+
@@ -26,25 +26,11 @@
-
+
-
-
-
-
-
-
-
-
-
@@ -52,7 +38,7 @@
-
+
@@ -93,7 +79,6 @@
-
@@ -204,6 +189,7 @@
+
@@ -248,6 +234,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo-ios-objc/demo-ios-objc/GAMBannerViewController.h b/demo-ios-objc/demo-ios-objc/GAMBannerViewController.h
index 4b7e5d2..0bdd4f7 100644
--- a/demo-ios-objc/demo-ios-objc/GAMBannerViewController.h
+++ b/demo-ios-objc/demo-ios-objc/GAMBannerViewController.h
@@ -7,8 +7,9 @@
//
#import
+#import "GoogleMobileAds/GADBannerViewDelegate.h"
-@interface GAMBannerViewController : UIViewController
+@interface GAMBannerViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIView *adPlaceholder;
diff --git a/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m b/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m
index fd27a96..ddd81fa 100644
--- a/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m
+++ b/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m
@@ -9,68 +9,96 @@
#import "OptableSDKDelegate.h"
#import "GAMBannerViewController.h"
#import "AppDelegate.h"
+#import "demo_ios_objc-Swift.h"
+
@import OptableSDK;
@import GoogleMobileAds;
@interface GAMBannerViewController ()
-
-@property(nonatomic, strong) GADBannerView *bannerView;
-
+// GoogleMobileAds - GADBannerView
+@property(nonatomic, strong) GADBannerView *gadBannerView;
+// Logging
+@property(nonatomic, strong, nullable) NSString* targetingLog;
+@property(nonatomic, strong, nullable) NSString* witnessLog;
+@property(nonatomic, strong, nullable) NSString* profileLog;
+@property(nonatomic, strong, nullable) id networkLogObserver;
@end
@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.rootViewController = self;
+ self.gadBannerView = [[GADBannerView alloc] initWithAdSize:GADAdSizeBanner];
+ self.gadBannerView.adUnitID = self.AD_MANAGER_AD_UNIT_ID;
+ self.gadBannerView.rootViewController = self;
+ self.gadBannerView.delegate = self;
+ [self addBannerViewToView:self.gadBannerView];
OptableSDKDelegate *delegate = (OptableSDKDelegate *)OPTABLE.delegate;
- delegate.bannerView = self.bannerView;
+ delegate.gadBannerView = self.gadBannerView;
+ delegate.pbmBannerAdUnit = nil;
delegate.targetingOutput = self.targetingOutput;
}
-- (IBAction)loadBannerWithTargeting:(id)sender {
- NSError *error = nil;
+- (void)viewWillAppear:(BOOL)animated {
+ [super viewWillAppear: animated];
+ [self startObservingNetworkLogs];
+}
- [_targetingOutput setText:@"Calling /targeting API...\n"];
+- (void)viewWillDisappear:(BOOL)animated {
+ [super viewWillDisappear: animated];
+ [self stopObservingNetworkLogs];
+}
- [OPTABLE targetingAndReturnError:&error];
- [OPTABLE witness:@"GAMBannerViewController.loadBannerClicked" properties:@{ @"example": @"value" } error:&error];
- [OPTABLE profileWithTraits:@{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES } error:&error];
+// MARK: - Actions
+- (IBAction)loadBannerWithTargeting:(id)sender {
+ NSError *error = nil;
+ [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 {
NSError *error = nil;
- GAMRequest *request = [GAMRequest request];
- NSDictionary *keyvals = nil;
-
- [_targetingOutput setText:@"Checking local targeting cache...\n\n"];
+ OptableTargeting *cachedOptableTargeting = [OPTABLE targetingFromCache];
- 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]];
+ if (cachedOptableTargeting != nil) {
+ NSLog(@"[OptableSDK] β
Cached targeting values found: %@", cachedOptableTargeting);
} else {
- [_targetingOutput setText:[NSString stringWithFormat:@"%@\nCache empty.\n",
- [_targetingOutput text]]];
+ NSLog(@"[OptableSDK] βΉοΈ Cache empty");
}
-
- [self.bannerView loadRequest:request];
- [OPTABLE witness:@"GAMBannerViewController.loadBannerClicked" properties:@{ @"example": @"value" } error:&error];
- [OPTABLE profileWithTraits:@{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES } error:&error];
+
+ [(OptableSDKDelegate*)OPTABLE.delegate loadGADAdWithTargetingData:cachedOptableTargeting];
+ [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 {
- [_targetingOutput setText:@"π§Ή Clearing local targeting cache.\n"];
+ [_targetingOutput setText:@"π§Ή Cleared local targeting cache.\n"];
[OPTABLE targetingClearCache];
}
+// MARK: - GADBannerViewDelegate
+- (void)bannerView:(GADBannerView *)bannerView didFailToReceiveAdWithError:(NSError *)error {
+ NSLog(@"[GAMBannerViewController] Failed to receive ad: %@", [error localizedDescription]);
+}
+
+// MARK: - Helpers
- (void)addBannerViewToView:(UIView *)bannerView {
bannerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.adPlaceholder addSubview:bannerView];
@@ -81,4 +109,70 @@ - (void)addBannerViewToView:(UIView *)bannerView {
]];
}
+- (void)updateUILog {
+ NSMutableArray *parts = [NSMutableArray array];
+
+ if (self.targetingLog) {
+ [parts addObject:self.targetingLog];
+ }
+ if (self.witnessLog) {
+ [parts addObject:self.witnessLog];
+ }
+ if (self.profileLog) {
+ [parts addObject:self.profileLog];
+ }
+
+ self.targetingOutput.text = [parts componentsJoinedByString:@"\n\n"];
+}
+
+// MARK: - Logging
+- (void)startObservingNetworkLogs {
+ __weak typeof(self) weakSelf = self;
+
+ _networkLogObserver = [NSNotificationCenter.defaultCenter
+ addObserverForName: NotificationNames.HTTPURLLogUpdated
+ object: nil
+ queue: [NSOperationQueue mainQueue]
+ usingBlock: ^(NSNotification* notification) {
+ HTTPURLLogEntry *logEntry = notification.userInfo[@"data"];
+ if (![logEntry isKindOfClass:[HTTPURLLogEntry class]]) {
+ return;
+ }
+
+ NSString *urlString = logEntry.request.URL.absoluteString;
+ if ([urlString containsString:@"/targeting"]) {
+ weakSelf.targetingLog = logEntry.debugDescription;
+ if (logEntry.response == nil) {
+ NSLog(@"%@", logEntry.requestDebugDescription);
+ } else {
+ NSLog(@"%@", logEntry.responseDebugDescription);
+ }
+ }
+ if ([urlString containsString:@"/witness"]) {
+ weakSelf.witnessLog = logEntry.debugDescription;
+ if (logEntry.response == nil) {
+ NSLog(@"%@", logEntry.requestDebugDescription);
+ } else {
+ NSLog(@"%@", logEntry.responseDebugDescription);
+ }
+ }
+ if ([urlString containsString:@"/profile"]) {
+ weakSelf.profileLog = logEntry.debugDescription;
+ if (logEntry.response == nil) {
+ NSLog(@"%@", logEntry.requestDebugDescription);
+ } else {
+ NSLog(@"%@", logEntry.responseDebugDescription);
+ }
+ }
+
+ [weakSelf updateUILog];
+ }];
+}
+
+- (void)stopObservingNetworkLogs {
+ if (_networkLogObserver != NULL) {
+ [NSNotificationCenter.defaultCenter removeObserver: _networkLogObserver];
+ }
+}
+
@end
diff --git a/demo-ios-objc/demo-ios-objc/IdentifyViewController.h b/demo-ios-objc/demo-ios-objc/IdentifyViewController.h
index 8eaba1c..055e2ea 100644
--- a/demo-ios-objc/demo-ios-objc/IdentifyViewController.h
+++ b/demo-ios-objc/demo-ios-objc/IdentifyViewController.h
@@ -8,10 +8,9 @@
#import
-@interface IdentifyViewController : UIViewController
+@interface IdentifyViewController : UIViewController
@property (weak, nonatomic) IBOutlet UITextField *identifyInput;
@property (weak, nonatomic) IBOutlet UIButton *identifyButton;
-@property (weak, nonatomic) IBOutlet UISwitch *identifyIDFA;
@property (weak, nonatomic) IBOutlet UITextView *identifyOutput;
- (IBAction)dispatchIdentify:(id)sender;
diff --git a/demo-ios-objc/demo-ios-objc/IdentifyViewController.m b/demo-ios-objc/demo-ios-objc/IdentifyViewController.m
index e046adb..ae7aaca 100644
--- a/demo-ios-objc/demo-ios-objc/IdentifyViewController.m
+++ b/demo-ios-objc/demo-ios-objc/IdentifyViewController.m
@@ -9,34 +9,97 @@
#import "OptableSDKDelegate.h"
#import "IdentifyViewController.h"
#import "AppDelegate.h"
+#import "demo_ios_objc-Swift.h"
+
@import OptableSDK;
@interface IdentifyViewController ()
+@property (nonatomic, strong, nullable) id networkLogObserver;
@end
@implementation IdentifyViewController
- (void)viewDidLoad {
[super viewDidLoad];
-
+
+ _identifyInput.delegate = self;
+
OptableSDKDelegate *delegate = (OptableSDKDelegate *)OPTABLE.delegate;
delegate.identifyOutput = self.identifyOutput;
}
+- (void)viewWillAppear:(BOOL)animated {
+ [super viewWillAppear: animated];
+ [self startObservingNetworkLogs];
+}
+
+- (void)viewWillDisappear:(BOOL)animated {
+ [super viewWillDisappear: animated];
+ [self stopObservingNetworkLogs];
+}
+
+// MARK: - Actions
- (IBAction)dispatchIdentify:(id)sender {
- NSString *email = [_identifyInput text];
- bool aaid = [_identifyIDFA isOn];
- NSMutableString *output;
+ [self.view endEditing: TRUE];
+
+ NSString *email = _identifyInput.text;
+
+ NSMutableString *output = [NSMutableString stringWithFormat: @"Calling /identify API with:\n\n"];
+ if (email.length > 0) {
+ [output appendString: [NSString stringWithFormat: @"Email: %@\n", email]];
+ }
+
+ _identifyOutput.text = output;
+
NSError *error = nil;
+ NSDictionary *ids = @{
+ @"e" : email,
+ @"p": @"+1234567890",
+ @"a": @"06DE8C6A-A431-4235-A262-E3A9C2CCEB34",
+ @"g": @"D04BB8C3-5A3E-4964-9757-D38365F59E6A",
+ @"c": @"new-custom.ABC",
+ @"c9": @"custom-9-id",
+ };
+ [OPTABLE identify: ids error: &error];
+}
- 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];
+// MARK: - UITextFieldDelegate
+- (BOOL)textFieldShouldReturn:(UITextField *)textField {
+ [textField resignFirstResponder];
+ return FALSE;
+}
- [OPTABLE identify :email aaid:aaid ppid:@"" error:&error];
+// MARK: - Logging
+- (void)startObservingNetworkLogs {
+ __weak typeof(self) weakSelf = self;
+
+ _networkLogObserver = [NSNotificationCenter.defaultCenter
+ addObserverForName: NotificationNames.HTTPURLLogUpdated
+ object: nil
+ queue: [NSOperationQueue mainQueue]
+ usingBlock: ^(NSNotification* notification) {
+ HTTPURLLogEntry *logEntry = notification.userInfo[@"data"];
+ if (![logEntry isKindOfClass:[HTTPURLLogEntry class]]) {
+ return;
+ }
+
+ NSString *urlString = logEntry.request.URL.absoluteString;
+ if ([urlString containsString:@"/identify"]) {
+ weakSelf.identifyOutput.text = logEntry.debugDescription;
+
+ if (logEntry.response == nil) {
+ NSLog(@"%@", logEntry.requestDebugDescription);
+ } else {
+ NSLog(@"%@", logEntry.responseDebugDescription);
+ }
+ }
+ }];
+}
+
+- (void)stopObservingNetworkLogs {
+ if (_networkLogObserver != NULL) {
+ [NSNotificationCenter.defaultCenter removeObserver: _networkLogObserver];
+ }
}
@end
diff --git a/demo-ios-objc/demo-ios-objc/Misc/HTTPURLLogProtocol.swift b/demo-ios-objc/demo-ios-objc/Misc/HTTPURLLogProtocol.swift
new file mode 100644
index 0000000..7ab9eb0
--- /dev/null
+++ b/demo-ios-objc/demo-ios-objc/Misc/HTTPURLLogProtocol.swift
@@ -0,0 +1,223 @@
+//
+// HTTPURLLogProtocol.swift
+// demo-ios-swift
+//
+// Copyright Β© 2026 Optable Technologies Inc. All rights reserved.
+// See LICENSE for details.
+//
+
+import Foundation
+
+// MARK: - HTTPURLLogProtocol
+/**
+ This protocol logs all requests/responses that are passed through URLSession
+ */
+@objc(HTTPURLLogProtocol)
+final class HTTPURLLogProtocol: URLProtocol {
+ override class func canInit(with request: URLRequest) -> Bool {
+ // Avoid infinite loop
+ if URLProtocol.property(forKey: "_logged_", in: request) != nil {
+ return false
+ }
+ return true
+ }
+
+ override class func canonicalRequest(for request: URLRequest) -> URLRequest {
+ request
+ }
+
+ override func startLoading() {
+ var request = ((self.request as NSURLRequest).mutableCopy() as? NSMutableURLRequest) ?? NSMutableURLRequest()
+ URLProtocol.setProperty(true, forKey: "_logged_", in: request)
+
+ let entryId = UUID()
+ let requestBody = captureBody(from: &request)
+ let logEntry = HTTPURLLogEntry(id: entryId, date: Date(), request: request as URLRequest, requestData: requestBody, response: nil, responseData: nil, error: nil)
+ HTTPURLLogStore.update(logEntry)
+
+ let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in
+ if let response = response as? HTTPURLResponse {
+ let updateLogEntry = HTTPURLLogEntry(id: entryId, date: Date(), request: request as URLRequest, requestData: requestBody, response: response, responseData: data, error: error)
+ HTTPURLLogStore.update(updateLogEntry)
+ }
+
+ // Pass-through the URL system
+
+ if let response {
+ self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
+ }
+
+ if let data {
+ self.client?.urlProtocol(self, didLoad: data)
+ }
+
+ if let error {
+ self.client?.urlProtocol(self, didFailWithError: error)
+ } else {
+ self.client?.urlProtocolDidFinishLoading(self)
+ }
+ }
+
+ task.resume()
+ }
+
+ override func stopLoading() {}
+}
+
+// MARK: - HTTPURLLogEntry
+@objc
+class HTTPURLLogEntry: NSObject, Identifiable {
+ @objc let id: UUID
+ @objc let date: Date
+ @objc let request: URLRequest
+ @objc let requestData: Data?
+ @objc let response: HTTPURLResponse?
+ @objc let responseData: Data?
+ @objc let error: Error?
+
+ init(id: UUID, date: Date, request: URLRequest, requestData: Data? = nil, response: HTTPURLResponse? = nil, responseData: Data? = nil, error: (any Error)? = nil) {
+ self.id = id
+ self.date = date
+ self.request = request
+ self.requestData = requestData
+ self.response = response
+ self.responseData = responseData
+ self.error = error
+ }
+
+ @objc override var debugDescription: String {
+ return "[HTTPURLLogEntry]\n"
+ + "πΉ RequestId: \(id.uuidString)\n"
+ + _requestDebugDescription
+ + "\n\n"
+ + _responseDebugDescription
+ }
+
+ @objc var requestDebugDescription: String {
+ return "[HTTPURLLogEntry]\n"
+ + "πΉ RequestId: \(id.uuidString)\n"
+ + _requestDebugDescription
+ }
+
+ @objc var responseDebugDescription: String {
+ return "[HTTPURLLogEntry]\n"
+ + "πΉ RequestId: \(id.uuidString)\n"
+ + _responseDebugDescription
+ }
+
+ private var _requestDebugDescription: String {
+ guard let requestHTTPMethod = request.httpMethod, let requestURL = request.url else {
+ return "βοΈ URLRequest is not complete"
+ }
+
+ var output = [String]()
+
+ output.append("β¬οΈ \(requestHTTPMethod) \(requestURL)")
+
+ if let httpHeaders = request.allHTTPHeaderFields?.sorted(by: { $0.key < $1.key }) {
+ output.append("πΈ Headers:")
+ output.append(contentsOf: httpHeaders.map({ "\t\($0): \($1)" }))
+ }
+
+ if let requestData {
+ output.append("πΉ Body:")
+ output.append("\t\(String(decoding: requestData, as: UTF8.self))")
+ }
+
+ return output.joined(separator: "\n")
+ }
+
+ private var _responseDebugDescription: String {
+ guard let response, let responseURL = response.url?.absoluteString else {
+ return "βοΈ URLResponse is not complete"
+ }
+
+ var output = [String]()
+
+ output.append("β¬οΈ \(response.statusCode) \(responseURL)")
+
+ if response.allHeaderFields.isEmpty == false, let httpHeaders = response.allHeaderFields as? [String: String] {
+ output.append("πΈ Headers:")
+ output.append(contentsOf: httpHeaders.sorted(by: { $0.key < $1.key }).map({ "\t\($0): \($1)" }))
+ }
+
+ if let responseData {
+ output.append("πΉ Body:")
+ output.append("\t\(String(decoding: responseData, as: UTF8.self))")
+ }
+
+ return output.joined(separator: "\n")
+ }
+}
+
+// MARK: - HTTPURLLogStore
+enum HTTPURLLogStore {
+ private static let queue = DispatchQueue(label: "network.log.store", attributes: .concurrent)
+ private static let kMaxCapacity: UInt = 20
+ private static var entries: [HTTPURLLogEntry] = []
+
+ fileprivate static func update(_ entry: HTTPURLLogEntry) {
+ queue.async(flags: .barrier) {
+ self.entries.append(entry)
+ if self.entries.count > self.kMaxCapacity {
+ self.entries.removeFirst()
+ }
+ }
+
+ NotificationCenter.default.post(name: NotificationNames.HTTPURLLogUpdated, object: nil, userInfo: ["data": entry])
+ }
+
+ static func all() -> [HTTPURLLogEntry] {
+ queue.sync { entries }
+ }
+
+ static func filter(_ predicate: (HTTPURLLogEntry) -> Bool) -> [HTTPURLLogEntry] {
+ queue.sync { entries.filter(predicate) }
+ }
+}
+
+// MARK: - NotificationNames
+@objc public final class NotificationNames: NSObject {
+ @objc public static let HTTPURLLogUpdated =
+ Notification.Name("HTTPURLLogUpdated")
+}
+
+// MARK: - Misc
+private func captureBody(from request: inout NSMutableURLRequest) -> Data? {
+ if let body = request.httpBody {
+ return body
+ }
+
+ if let stream = request.httpBodyStream {
+ let data = drainStream(stream)
+ request.httpBodyStream = InputStream(data: data)
+ return data
+ }
+
+ return nil
+}
+
+private func drainStream(_ stream: InputStream) -> Data {
+ stream.open()
+ defer { stream.close() }
+
+ var data = Data()
+ let bufferSize = 8 * 1024
+ var buffer = [UInt8](repeating: 0, count: bufferSize)
+
+ while true {
+ let bytesRead = stream.read(&buffer, maxLength: bufferSize)
+
+ if bytesRead > 0 {
+ data.append(buffer, count: bytesRead)
+ } else if bytesRead == 0 {
+ // End of stream
+ break
+ } else {
+ // Error occurred
+ break
+ }
+ }
+
+ return data
+}
diff --git a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.h b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.h
index ae1a2f1..a6aad24 100644
--- a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.h
+++ b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.h
@@ -7,12 +7,26 @@
//
@import OptableSDK;
+
+@import PrebidMobile;
@import GoogleMobileAds;
@interface OptableSDKDelegate: NSObject
-@property(atomic, readwrite, strong) GADBannerView *bannerView;
-@property(atomic, readwrite, strong) UITextView *identifyOutput;
-@property(atomic, readwrite, strong) UITextView *targetingOutput;
+// MARK: - PrebidMobile
+@property(atomic, readwrite, weak, nullable) BannerAdUnit *pbmBannerAdUnit;
+
+// MARK: - GoogleMobileAds
+@property(atomic, readwrite, weak, nullable) GADBannerView *gadBannerView;
+
+// MARK: - Text Output
+@property(atomic, readwrite, strong, nullable) UITextView *identifyOutput;
+@property(atomic, readwrite, strong, nullable) UITextView *targetingOutput;
+
+// MARK: - Ad Loading
+- (void)loadGADAdWithTargetingData:(OptableTargeting* _Nullable)optableTargeting;
+- (void)loadPrebidAdWithTargetingData:(OptableTargeting* _Nullable)optableTargeting;
+- (void)setOptableTargetingToPrebidWith:(OptableTargeting* _Nullable)optableTargeting;
+- (void)loadGADAdWithAdRequest:(GAMRequest* _Nonnull)adRequest;
@end
diff --git a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m
index 67e0774..9e0ad6d 100644
--- a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m
+++ b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m
@@ -9,70 +9,90 @@
#import "OptableSDKDelegate.h"
@import OptableSDK;
@import GoogleMobileAds;
+@import PrebidMobile;
-@interface OptableSDKDelegate ()
-@end
-
+// MARK: - 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]]];
- });
+ NSLog(@"[OptableSDK] β
Success on /identify API call");
}
+
- (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]]];
- });
+ NSLog(@"[OptableSDK] π« Error on /identify API call: %@", [error localizedDescription]);
}
+
- (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]]];
- });
+ NSLog(@"[OptableSDK] β
Success on /profile API call");
}
+
- (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]]];
- });
+ NSLog(@"[OptableSDK] π« Error on /profile API call: %@", [error localizedDescription]);
}
-- (void)targetingOk:(NSDictionary *)result {
- // Update the GAM banner view with result targeting keyvalues:
- 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]];
- });
+- (void)targetingOk:(OptableTargeting *)result {
+ NSLog(@"[OptableSDK] β
Success on /targeting API call: %@", result.debugDescription);
+
+ if (_pbmBannerAdUnit != nil) {
+ // PrebidBannerViewController
+ [self loadPrebidAdWithTargetingData:result];
+ } else {
+ // GAMBannerViewController
+ [self loadGADAdWithTargetingData:result];
+ }
}
+
- (void)targetingErr:(NSError *)error {
+ NSLog(@"[OptableSDK] π« Error on /targeting API call: %@", [error localizedDescription]);
+
// 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]]];
- });
+ [self loadGADAdWithTargetingData:nil];
}
+
- (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]]];
- });
+ NSLog(@"[OptableSDK] β
Success on /witness API call");
}
+
- (void)witnessErr:(NSError *)error {
- NSLog(@"[OptableSDK] Error on /witness API call: %@", [error localizedDescription]);
+ NSLog(@"[OptableSDK] π« Error on /witness API call: %@", [error localizedDescription]);
+}
+
+// MARK: - Ad Loading
+- (void)loadGADAdWithTargetingData:(OptableTargeting* _Nullable)optableTargeting {
+ GAMRequest *adRequest = [GAMRequest request];
+
+ if (optableTargeting != nil && optableTargeting.gamTargetingKeywords != nil) {
+ adRequest.customTargeting = optableTargeting.gamTargetingKeywords;
+ }
+
+ [self loadGADAdWithAdRequest:adRequest];
+}
+
+- (void)loadPrebidAdWithTargetingData:(OptableTargeting* _Nullable)optableTargeting {
+ [self setOptableTargetingToPrebidWith:optableTargeting];
+
+ GAMRequest *adRequest = [GAMRequest request];
- dispatch_async(dispatch_get_main_queue(), ^{
- [self.targetingOutput setText:[NSString stringWithFormat:@"%@\nπ« Error: %@\n", [self.targetingOutput text], [error localizedDescription]]];
- });
+ if (optableTargeting != nil && optableTargeting.gamTargetingKeywords != nil) {
+ adRequest.customTargeting = optableTargeting.gamTargetingKeywords;
+ }
+
+ [_pbmBannerAdUnit fetchDemandWithAdObject:adRequest completion: ^(enum ResultCode result) {
+ NSLog(@"[PrebidMobile]:fetchDemand(adObject:): %ld", (long)result);
+ [self loadGADAdWithAdRequest:adRequest];
+ }];
+}
+
+- (void)setOptableTargetingToPrebidWith:(OptableTargeting* _Nullable)optableTargeting {
+ if (optableTargeting == nil || optableTargeting.ortb2 == nil || optableTargeting.ortb2.length == 0) {
+ [Targeting.shared setGlobalORTBConfig:nil];
+ return;
+ }
+
+ [Targeting.shared setGlobalORTBConfig:optableTargeting.ortb2];
}
+
+- (void)loadGADAdWithAdRequest:(GAMRequest*)adRequest {
+ [_gadBannerView loadRequest:adRequest];
+}
+
@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..c55a390
--- /dev/null
+++ b/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.h
@@ -0,0 +1,23 @@
+//
+// PrebidBannerViewController.h
+// demo-ios-objc
+//
+// Copyright Β© 2020 Optable Technologies Inc. All rights reserved.
+// See LICENSE for details.
+//
+
+#import
+#import "GoogleMobileAds/GADBannerViewDelegate.h"
+
+@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..4130fe7
--- /dev/null
+++ b/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.m
@@ -0,0 +1,187 @@
+//
+// 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 "demo_ios_objc-Swift.h"
+
+@import OptableSDK;
+@import GoogleMobileAds;
+
+@interface PrebidBannerViewController ()
+// GoogleMobileAds - GADBannerView
+@property(nonatomic, strong) GADBannerView *gadBannerView;
+// PrebidMobile
+@property(nonatomic, strong) BannerAdUnit *pbmBannerAdUnit;
+// Logging
+@property(nonatomic, strong, nullable) NSString* targetingLog;
+@property(nonatomic, strong, nullable) NSString* witnessLog;
+@property(nonatomic, strong, nullable) NSString* profileLog;
+@property(nonatomic, strong, nullable) id networkLogObserver;
+@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.gadBannerView = [[GADBannerView alloc] initWithAdSize:GADAdSizeBanner];
+ self.gadBannerView.adUnitID = self.AD_MANAGER_AD_UNIT_ID;
+ self.gadBannerView.rootViewController = self;
+ [self addBannerViewToView:self.gadBannerView];
+
+ self.pbmBannerAdUnit = [[BannerAdUnit alloc] initWithConfigId:self.PREBID_STORED_IMP
+ size:CGSizeMake(320, 50)];
+
+ OptableSDKDelegate *delegate = (OptableSDKDelegate *)OPTABLE.delegate;
+ delegate.gadBannerView = self.gadBannerView;
+ delegate.pbmBannerAdUnit = self.pbmBannerAdUnit;
+ delegate.targetingOutput = self.targetingOutput;
+}
+
+- (void)viewWillAppear:(BOOL)animated {
+ [super viewWillAppear: animated];
+ [self startObservingNetworkLogs];
+}
+
+- (void)viewWillDisappear:(BOOL)animated {
+ [super viewWillDisappear: animated];
+ [self stopObservingNetworkLogs];
+}
+
+// MARK: - Actions
+- (IBAction)loadBannerWithTargeting:(id)sender {
+ NSError *error = nil;
+ [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;
+ OptableTargeting *cachedOptableTargeting = [OPTABLE targetingFromCache];
+
+ if (cachedOptableTargeting != nil) {
+ NSLog(@"[OptableSDK] β
Cached targeting values found: %@", cachedOptableTargeting);
+ } else {
+ NSLog(@"[OptableSDK] βΉοΈ Cache empty");
+ }
+
+ [(OptableSDKDelegate*)OPTABLE.delegate loadPrebidAdWithTargetingData:cachedOptableTargeting];
+ [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: - GADBannerViewDelegate
+- (void)bannerView:(GADBannerView *)bannerView didFailToReceiveAdWithError:(NSError *)error {
+ NSLog(@"[GAMBannerViewController] Failed to receive ad: %@", [error localizedDescription]);
+}
+
+// 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]
+ ]];
+}
+
+- (void)updateUILog {
+ NSMutableArray *parts = [NSMutableArray array];
+
+ if (self.targetingLog) {
+ [parts addObject:self.targetingLog];
+ }
+ if (self.witnessLog) {
+ [parts addObject:self.witnessLog];
+ }
+ if (self.profileLog) {
+ [parts addObject:self.profileLog];
+ }
+
+ self.targetingOutput.text = [parts componentsJoinedByString:@"\n\n"];
+}
+
+// MARK: - Logging
+- (void)startObservingNetworkLogs {
+ __weak typeof(self) weakSelf = self;
+
+ _networkLogObserver = [NSNotificationCenter.defaultCenter
+ addObserverForName: NotificationNames.HTTPURLLogUpdated
+ object: nil
+ queue: [NSOperationQueue mainQueue]
+ usingBlock: ^(NSNotification* notification) {
+ HTTPURLLogEntry *logEntry = notification.userInfo[@"data"];
+ if (![logEntry isKindOfClass:[HTTPURLLogEntry class]]) {
+ return;
+ }
+
+ NSString *urlString = logEntry.request.URL.absoluteString;
+ if ([urlString containsString:@"/targeting"]) {
+ weakSelf.targetingLog = logEntry.debugDescription;
+ if (logEntry.response == nil) {
+ NSLog(@"%@", logEntry.requestDebugDescription);
+ } else {
+ NSLog(@"%@", logEntry.responseDebugDescription);
+ }
+ }
+ if ([urlString containsString:@"/witness"]) {
+ weakSelf.witnessLog = logEntry.debugDescription;
+ if (logEntry.response == nil) {
+ NSLog(@"%@", logEntry.requestDebugDescription);
+ } else {
+ NSLog(@"%@", logEntry.responseDebugDescription);
+ }
+ }
+ if ([urlString containsString:@"/profile"]) {
+ weakSelf.profileLog = logEntry.debugDescription;
+ if (logEntry.response == nil) {
+ NSLog(@"%@", logEntry.requestDebugDescription);
+ } else {
+ NSLog(@"%@", logEntry.responseDebugDescription);
+ }
+ }
+
+ [weakSelf updateUILog];
+ }];
+}
+
+- (void)stopObservingNetworkLogs {
+ if (_networkLogObserver != NULL) {
+ [NSNotificationCenter.defaultCenter removeObserver: _networkLogObserver];
+ }
+}
+
+@end
diff --git a/demo-ios-objc/demo-ios-objc/demo-ios-objc-Bridging-Header.h b/demo-ios-objc/demo-ios-objc/demo-ios-objc-Bridging-Header.h
new file mode 100644
index 0000000..38367ae
--- /dev/null
+++ b/demo-ios-objc/demo-ios-objc/demo-ios-objc-Bridging-Header.h
@@ -0,0 +1,13 @@
+//
+// demo-ios-objc-Bridging-Header.h
+// demo-ios-objc
+//
+// Copyright Β© 2026 Optable Technologies Inc. All rights reserved.
+// See LICENSE for details.
+//
+
+#ifndef demo_ios_objc_Bridging_Header_h
+#define demo_ios_objc_Bridging_Header_h
+
+
+#endif /* demo_ios_objc_Bridging_Header_h */
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
diff --git a/demo-ios-swift/Podfile b/demo-ios-swift/Podfile
index 15aa921..5e9d12e 100644
--- a/demo-ios-swift/Podfile
+++ b/demo-ios-swift/Podfile
@@ -1,8 +1,4 @@
-platform :ios, '14.0'
-
-source 'https://cdn.cocoapods.org/'
-
-project 'demo-ios-swift.xcodeproj'
+platform :ios, '15.0'
target 'demo-ios-swift' do
use_frameworks!
@@ -12,14 +8,6 @@ target 'demo-ios-swift' do
#pod 'OptableSDK'
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
+ pod 'PrebidMobile'
end
diff --git a/demo-ios-swift/Podfile.lock b/demo-ios-swift/Podfile.lock
index e77c5d6..619e80c 100644
--- a/demo-ios-swift/Podfile.lock
+++ b/demo-ios-swift/Podfile.lock
@@ -1,27 +1,33 @@
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)
+ - 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:
:path: "../"
SPEC CHECKSUMS:
- Google-Mobile-Ads-SDK: b833c723759e32bbaf06edaaf2293f08ed898232
- GoogleUserMessagingPlatform: f8d0cdad3ca835406755d0a69aa634f00e76d576
- OptableSDK: fc5d3852c29fac1881b1d3ab6ea397de71c8cbf1
+ Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2
+ GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c
+ OptableSDK: ce87b57c73d9324438f555a7e2b1b6da2c33069a
+ PrebidMobile: 046bb6220157c7332dc6c6e19a99397bb481ac3a
-PODFILE CHECKSUM: a7bf67fad0ec61ca3a95f0f8a9aadf2c4fd2cd76
+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 f4e17c3..027ad06 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 */; };
@@ -15,29 +16,9 @@
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 */; };
+ CE144EDE2F11397C00C787D8 /* HTTPURLLogProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE144EDD2F11397C00C787D8 /* HTTPURLLogProtocol.swift */; };
/* 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;
@@ -54,6 +35,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 = ""; };
@@ -63,17 +45,12 @@
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 = ""; };
AFE041B55E3398A6DDE84BFF /* Pods-demo-ios-swiftTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-swiftTests.release.xcconfig"; path = "Target Support Files/Pods-demo-ios-swiftTests/Pods-demo-ios-swiftTests.release.xcconfig"; sourceTree = ""; };
BEEBA234E54019EC38C70912 /* Pods-demo-ios-swiftTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-swiftTests.debug.xcconfig"; path = "Target Support Files/Pods-demo-ios-swiftTests/Pods-demo-ios-swiftTests.debug.xcconfig"; sourceTree = ""; };
+ CE144EDD2F11397C00C787D8 /* HTTPURLLogProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLLogProtocol.swift; sourceTree = ""; };
D76144A328FE0069ABDF4B5F /* Pods_demo_ios_swift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_demo_ios_swift.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E8E21CA91246EF005B351B37 /* Pods-demo-ios-swift-demo-ios-swiftUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-swift-demo-ios-swiftUITests.release.xcconfig"; path = "Target Support Files/Pods-demo-ios-swift-demo-ios-swiftUITests/Pods-demo-ios-swift-demo-ios-swiftUITests.release.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
@@ -87,22 +64,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 +71,6 @@
isa = PBXGroup;
children = (
6352AA5924DC7AE9002E66EB /* demo-ios-swift */,
- 6352AA7024DC7AEC002E66EB /* demo-ios-swiftTests */,
- 6352AA7B24DC7AEC002E66EB /* demo-ios-swiftUITests */,
6352AA5824DC7AE9002E66EB /* Products */,
63C1E32324EAD73800C4FE51 /* Frameworks */,
8018ABD26BD652CFB14910C3 /* Pods */,
@@ -122,8 +81,6 @@
isa = PBXGroup;
children = (
6352AA5724DC7AE9002E66EB /* demo-ios-swift.app */,
- 6352AA6D24DC7AEC002E66EB /* demo-ios-swiftTests.xctest */,
- 6352AA7824DC7AEC002E66EB /* demo-ios-swiftUITests.xctest */,
);
name = Products;
sourceTree = "";
@@ -133,34 +90,18 @@
children = (
6352AA5A24DC7AE9002E66EB /* AppDelegate.swift */,
6352AA5C24DC7AE9002E66EB /* SceneDelegate.swift */,
- 631466EC24F7F555007DCA5D /* GAMBannerViewController.swift */,
6352AA5E24DC7AE9002E66EB /* IdentifyViewController.swift */,
+ 631466EC24F7F555007DCA5D /* GAMBannerViewController.swift */,
+ 536D9E912EA0DD37006D86BE /* PrebidBannerViewController.swift */,
6352AA6024DC7AE9002E66EB /* Main.storyboard */,
6352AA6324DC7AEC002E66EB /* Assets.xcassets */,
6352AA6524DC7AEC002E66EB /* LaunchScreen.storyboard */,
6352AA6824DC7AEC002E66EB /* Info.plist */,
+ CE144EDF2F113FD500C787D8 /* Misc */,
);
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 = (
@@ -184,6 +125,14 @@
path = Pods;
sourceTree = "";
};
+ CE144EDF2F113FD500C787D8 /* Misc */ = {
+ isa = PBXGroup;
+ children = (
+ CE144EDD2F11397C00C787D8 /* HTTPURLLogProtocol.swift */,
+ );
+ path = Misc;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -208,46 +157,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 +170,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 +186,6 @@
projectRoot = "";
targets = (
6352AA5624DC7AE9002E66EB /* demo-ios-swift */,
- 6352AA6C24DC7AEC002E66EB /* demo-ios-swiftTests */,
- 6352AA7724DC7AEC002E66EB /* demo-ios-swiftUITests */,
);
};
/* End PBXProject section */
@@ -302,67 +201,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 +226,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;
@@ -410,30 +234,17 @@
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",
);
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- 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",
+ outputPaths = (
);
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";
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
E4BF60F1588582695FC58696 /* [CP] Copy Pods Resources */ = {
@@ -444,10 +255,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";
@@ -461,43 +276,16 @@
buildActionMask = 2147483647;
files = (
6352AA5F24DC7AE9002E66EB /* IdentifyViewController.swift in Sources */,
+ 536D9E922EA0DD37006D86BE /* PrebidBannerViewController.swift in Sources */,
6352AA5B24DC7AE9002E66EB /* AppDelegate.swift in Sources */,
+ CE144EDE2F11397C00C787D8 /* HTTPURLLogProtocol.swift in Sources */,
631466ED24F7F555007DCA5D /* GAMBannerViewController.swift in Sources */,
6352AA5D24DC7AE9002E66EB /* SceneDelegate.swift in Sources */,
);
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 +458,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 +479,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..116b410 100644
--- a/demo-ios-swift/demo-ios-swift/AppDelegate.swift
+++ b/demo-ios-swift/demo-ios-swift/AppDelegate.swift
@@ -6,8 +6,11 @@
// See LICENSE for details.
//
-import UIKit
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
@@ -16,18 +19,41 @@ 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")
+
+ // Debug URLSession
+ URLProtocol.registerClass(HTTPURLLogProtocol.self)
+
+ let config = OptableConfig(
+ tenant: "prebidtest",
+ originSlug: "ios-sdk",
+ host: "prebidtest.cloud.optable.co",
+ 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..bb13586 100644
--- a/demo-ios-swift/demo-ios-swift/Base.lproj/Main.storyboard
+++ b/demo-ios-swift/demo-ios-swift/Base.lproj/Main.storyboard
@@ -1,9 +1,9 @@
-
+
-
+
@@ -18,7 +18,7 @@
-
+
@@ -26,25 +26,11 @@
-
+
-
-
-
-
-
-
-
-
-
@@ -52,7 +38,7 @@
-
+
@@ -93,7 +79,6 @@
-
@@ -204,6 +189,7 @@
+
@@ -248,6 +234,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift b/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift
index 4a8fa0d..0e48f0c 100644
--- a/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift
+++ b/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift
@@ -6,146 +6,190 @@
// See LICENSE for details.
//
-import UIKit
import GoogleMobileAds
+import OptableSDK
+import UIKit
+
+private let AD_MANAGER_AD_UNIT_ID = "/22081946781/ios-sdk-demo/mobile-leaderboard"
+
+// MARK: - GAMBannerViewController
+final class GAMBannerViewController: UIViewController {
+ // Outlets
+ @IBOutlet var adPlaceholder: UIView!
+ @IBOutlet var loadBannerButton: UIButton!
+ @IBOutlet var loadBannerFromCacheButton: UIButton!
+ @IBOutlet var clearTargetingCacheButton: UIButton!
+ @IBOutlet var targetingOutput: UITextView!
+
+ // GoogleMobileAds - GADBannerView
+ var gadBannerView: BannerView!
+
+ // Logging
+ private var targetingLog: String? { didSet { updateUILog() } }
+ private var witnessLog: String? { didSet { updateUILog() } }
+ private var profileLog: String? { didSet { updateUILog() } }
+ private var networkLogObserver: (any NSObjectProtocol)?
-class GAMBannerViewController: UIViewController {
-
- var bannerView: BannerView!
-
- // MARK: Properties
-
- @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()
-
- bannerView = BannerView(adSize: AdSizeBanner)
- addBannerViewToView(bannerView)
- bannerView.rootViewController = self
+
+ gadBannerView = BannerView(adSize: AdSizeBanner)
+ gadBannerView.adUnitID = AD_MANAGER_AD_UNIT_ID
+ gadBannerView.rootViewController = self
+ gadBannerView.delegate = self
+ addBannerViewToView(gadBannerView)
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ startObservingNetworkLogs()
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ stopObservingNetworkLogs()
}
-
- //MARK: Actions
-
+
+ // MARK: Actions
@IBAction func loadBannerWithTargeting(_ sender: UIButton) {
do {
- targetingOutput.text = "Calling /targeting API...\n\n"
-
- try OPTABLE!.targeting() { result in
- var tdata: NSDictionary = [:]
-
+ try OPTABLE!.targeting { [weak self] result in
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"
- }
-
- case .failure(let error):
- print("[OptableSDK] Error on /targeting API call: \(error)")
- DispatchQueue.main.async {
- self.targetingOutput.text += "π« Error: \(error)\n"
- }
+ case let .success(optableTargeting):
+ print("[OptableSDK] β
Success on /targeting API call: \(optableTargeting)")
+ self?.loadBanner(optableTargeting)
+
+ case let .failure(error):
+ print("[OptableSDK] π« Error on /targeting API call: \(error)")
+ self?.loadBanner()
}
-
- self.loadBanner(adUnitID: "/22081946781/ios-sdk-demo/mobile-leaderboard", keyvalues: tdata)
}
} catch {
- print("[OptableSDK] Exception: \(error)")
+ print("[OptableSDK] π« Exception on /targeting API call: \(error)")
+ targetingLog = "π« EXCEPTION: \(error)"
}
}
-
+
@IBAction func loadBannerWithTargetingFromCache(_ sender: UIButton) {
- var tdata: NSDictionary = [:]
-
- targetingOutput.text = "Checking local targeting cache...\n\n"
-
- 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 cachedOptableTargeting = OPTABLE!.targetingFromCache() {
+ print("[OptableSDK] β
Cached targeting values found: \(cachedOptableTargeting)")
+ loadBanner(cachedOptableTargeting)
} else {
- targetingOutput.text += "\nCache empty.\n"
+ print("[OptableSDK] βΉοΈ Cache empty")
+ loadBanner()
}
-
- self.loadBanner(adUnitID: "/22081946781/ios-sdk-demo/mobile-leaderboard", keyvalues: tdata)
}
-
+
@IBAction func clearTargetingCache(_ sender: UIButton) {
- targetingOutput.text = "π§Ή Clearing local targeting cache.\n"
+ targetingOutput.text = "π§Ή Cleared local targeting cache.\n"
OPTABLE!.targetingClearCache()
}
-
- private func loadBanner(adUnitID: String, keyvalues: NSDictionary) {
- bannerView.adUnitID = adUnitID
-
- let req = AdManagerRequest()
- req.customTargeting = keyvalues as? [String: String]
- bannerView.load(req)
-
+}
+
+// MARK: - Private
+private extension GAMBannerViewController {
+ func loadBanner(_ optableTargeting: OptableTargeting? = nil) {
+ loadGADAd(optableTargeting)
witness()
profile()
}
-
- private func witness() {
+
+ func loadGADAd(_ optableTargeting: OptableTargeting? = nil) {
+ let adRequest = AdManagerRequest()
+ adRequest.customTargeting = optableTargeting?.gamTargetingKeywords as? [String: Any]
+ gadBannerView.load(adRequest)
+ }
+
+ func witness() {
do {
- try OPTABLE!.witness(event: "GAMBannerViewController.loadBannerClicked", properties: ["example": "value"]) { result in
+ try OPTABLE!.witness(
+ event: "GAMBannerViewController.loadBannerClicked",
+ properties: ["example": "value"]
+ ) { 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 .failure(let error):
- print("[OptableSDK] Error on /witness API call: \(error)")
- DispatchQueue.main.async {
- self.targetingOutput.text += "\nπ« Error: \(error)"
- }
+ case .success:
+ print("[OptableSDK] β
Success on /witness API call")
+ case let .failure(error):
+ print("[OptableSDK] π« Error on /witness API call: \(error)")
}
}
} catch {
- print("[OptableSDK] Exception: \(error)")
+ print("[OptableSDK] π« Exception on /witness API call: \(error)")
+ witnessLog = "π« EXCEPTION: \(error)"
}
}
-
- private func profile() {
+
+ 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]
+ ) { 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 .failure(let error):
- print("[OptableSDK] Error on /profile API call: \(error)")
- DispatchQueue.main.async {
- self.targetingOutput.text += "\nπ« Error: \(error)"
- }
+ case .success:
+ print("[OptableSDK] β
Success on /profile API call")
+ case let .failure(error):
+ print("[OptableSDK] π« Error on /profile API call: \(error)")
}
}
} catch {
- print("[OptableSDK] Exception: \(error)")
+ print("[OptableSDK] π« Exception on /profile API call: \(error)")
+ profileLog = "π« EXCEPTION: \(error)"
}
}
-
- private func addBannerViewToView(_ bannerView: BannerView) {
+}
+
+// MARK: - GoogleMobileAds.BannerViewDelegate
+extension GAMBannerViewController: GoogleMobileAds.BannerViewDelegate {
+ func bannerView(_ bannerView: BannerView, didFailToReceiveAdWithError error: any Error) {
+ print("[GAMBannerViewController] Failed to receive ad: \(error)")
+ }
+}
+
+// MARK: - Helpers
+private extension GAMBannerViewController {
+ func addBannerViewToView(_ bannerView: BannerView) {
bannerView.translatesAutoresizingMaskIntoConstraints = false
adPlaceholder.addSubview(bannerView)
-
+
NSLayoutConstraint.activate([
bannerView.centerXAnchor.constraint(equalTo: adPlaceholder.centerXAnchor),
- bannerView.centerYAnchor.constraint(equalTo: adPlaceholder.centerYAnchor)
+ bannerView.centerYAnchor.constraint(equalTo: adPlaceholder.centerYAnchor),
])
}
+
+ func updateUILog() {
+ targetingOutput.text = [targetingLog, witnessLog, profileLog].compactMap({ $0 }).joined(separator: "\n\n")
+ }
+}
+
+// MARK: - Logging
+private extension GAMBannerViewController {
+ /// Setups observation for URLRequest/URLResponse logging
+ func startObservingNetworkLogs() {
+ networkLogObserver = NotificationCenter.default
+ .addObserver(forName: .HTTPURLLogUpdated, object: nil, queue: .main, using: { [weak self] notification in
+ if let logEntry = notification.userInfo?["data"] as? HTTPURLLogEntry,
+ logEntry.request.url?.absoluteString.contains("/targeting") == true {
+ self?.targetingLog = logEntry.debugDescription
+ print(logEntry.response == nil ? logEntry.requestDebugDescription : logEntry.responseDebugDescription)
+ }
+
+ if let logEntry = notification.userInfo?["data"] as? HTTPURLLogEntry,
+ logEntry.request.url?.absoluteString.contains("/witness") == true {
+ self?.witnessLog = logEntry.debugDescription
+ print(logEntry.response == nil ? logEntry.requestDebugDescription : logEntry.responseDebugDescription)
+ }
+
+ if let logEntry = notification.userInfo?["data"] as? HTTPURLLogEntry,
+ logEntry.request.url?.absoluteString.contains("/profile") == true {
+ self?.profileLog = logEntry.debugDescription
+ print(logEntry.response == nil ? logEntry.requestDebugDescription : logEntry.responseDebugDescription)
+ }
+ })
+ }
+
+ func stopObservingNetworkLogs() {
+ guard let networkLogObserver else { return }
+ NotificationCenter.default.removeObserver(networkLogObserver)
+ }
}
diff --git a/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift b/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift
index cd11738..c494265 100644
--- a/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift
+++ b/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift
@@ -6,50 +6,94 @@
// See LICENSE for details.
//
+import OptableSDK
import UIKit
+// MARK: - IdentifyViewController
class IdentifyViewController: UIViewController {
+ // Outlets
+ @IBOutlet var identifyInput: UITextField!
+ @IBOutlet var identifyButton: UIButton!
+ @IBOutlet var identifyOutput: UITextView!
- // MARK: Properties
- @IBOutlet weak var identifyInput: UITextField!
- @IBOutlet weak var identifyButton: UIButton!
- @IBOutlet weak var identifyIDFA: UISwitch!
- @IBOutlet weak var identifyOutput: UITextView!
+ // Logging
+ private var networkLogObserver: (any NSObjectProtocol)?
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ identifyInput.delegate = self
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ startObservingNetworkLogs()
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ stopObservingNetworkLogs()
+ }
// 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.
+ // 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) {
+ view.endEditing(true)
+
do {
- let email = identifyInput.text! as String
- let aaid = identifyIDFA.isOn as Bool
+ let email = identifyInput.text
- identifyOutput.text = "Calling /identify API with:\n\n"
- if (email != "") {
- identifyOutput.text += "Email: " + email + "\n"
- }
- identifyOutput.text += "IDFA: " + String(aaid) + "\n"
+ let optableIdentifiers = OptableIdentifiers(
+ emailAddress: email,
+ phoneNumber: "+1234567890",
+ appleIDFA: "06DE8C6A-A431-4235-A262-E3A9C2CCEB34",
+ googleGAID: "D04BB8C3-5A3E-4964-9757-D38365F59E6A",
+ custom: [
+ "c": "new-custom.ABC",
+ "c9": "custom-9-id",
+ ]
+ )
- try OPTABLE!.identify(email: email, aaid: aaid) { result in
+ try OPTABLE!.identify(optableIdentifiers) { 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."
- }
-
- case .failure(let error):
- print("[OptableSDK] Error on /identify API call: \(error)")
- DispatchQueue.main.async {
- self.identifyOutput.text += "\nπ« Error: \(error)"
- }
+ case .success:
+ print("[OptableSDK] β
Success on /identify API call")
+ case let .failure(error):
+ print("[OptableSDK] π« Error on /identify API call: \(error)")
}
}
} catch {
- print("[OptableSDK] Exception: \(error)")
- identifyOutput.text += "EXCEPTION: \(error)"
+ print("[OptableSDK] π« Exception on /identify API call: \(error)")
+ identifyOutput.text += "π« EXCEPTION: \(error)"
}
}
}
+
+// MARK: - UITextFieldDelegate
+extension IdentifyViewController: UITextFieldDelegate {
+ func textFieldShouldReturn(_ textField: UITextField) -> Bool {
+ textField.resignFirstResponder()
+ return false
+ }
+}
+
+// MARK: - Logging
+private extension IdentifyViewController {
+ /// Setups observation for URLRequest/URLResponse logging
+ func startObservingNetworkLogs() {
+ networkLogObserver = NotificationCenter.default
+ .addObserver(forName: .HTTPURLLogUpdated, object: nil, queue: .main, using: { [weak self] notification in
+ if let logEntry = notification.userInfo?["data"] as? HTTPURLLogEntry,
+ logEntry.request.url?.absoluteString.contains("/identify") == true {
+ self?.identifyOutput.text = logEntry.debugDescription
+ print(logEntry.response == nil ? logEntry.requestDebugDescription : logEntry.responseDebugDescription)
+ }
+ })
+ }
+
+ func stopObservingNetworkLogs() {
+ guard let networkLogObserver else { return }
+ NotificationCenter.default.removeObserver(networkLogObserver)
+ }
+}
diff --git a/demo-ios-swift/demo-ios-swift/Misc/HTTPURLLogProtocol.swift b/demo-ios-swift/demo-ios-swift/Misc/HTTPURLLogProtocol.swift
new file mode 100644
index 0000000..6cd0816
--- /dev/null
+++ b/demo-ios-swift/demo-ios-swift/Misc/HTTPURLLogProtocol.swift
@@ -0,0 +1,209 @@
+//
+// HTTPURLLogProtocol.swift
+// demo-ios-swift
+//
+// Copyright Β© 2026 Optable Technologies Inc. All rights reserved.
+// See LICENSE for details.
+//
+
+import Foundation
+
+// MARK: - HTTPURLLogProtocol
+/**
+ This protocol logs all requests/responses that are passed through URLSession
+ */
+final class HTTPURLLogProtocol: URLProtocol {
+ override class func canInit(with request: URLRequest) -> Bool {
+ // Avoid infinite loop by setting flag
+ if URLProtocol.property(forKey: "_logged_", in: request) != nil {
+ return false
+ }
+ return true
+ }
+
+ override class func canonicalRequest(for request: URLRequest) -> URLRequest {
+ request
+ }
+
+ override func startLoading() {
+ var request = ((self.request as NSURLRequest).mutableCopy() as? NSMutableURLRequest) ?? NSMutableURLRequest()
+ URLProtocol.setProperty(true, forKey: "_logged_", in: request)
+
+ let entryId = UUID()
+ let requestBody = captureBody(from: &request)
+ let logEntry = HTTPURLLogEntry(id: entryId, date: Date(), request: request as URLRequest, requestData: requestBody, response: nil, responseData: nil, error: nil)
+ HTTPURLLogStore.update(logEntry)
+
+ let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in
+ if let response = response as? HTTPURLResponse {
+ let updateLogEntry = HTTPURLLogEntry(id: entryId, date: Date(), request: request as URLRequest, requestData: requestBody, response: response, responseData: data, error: error)
+ HTTPURLLogStore.update(updateLogEntry)
+ }
+
+ // Pass-through the URL system
+
+ if let response {
+ self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
+ }
+
+ if let data {
+ self.client?.urlProtocol(self, didLoad: data)
+ }
+
+ if let error {
+ self.client?.urlProtocol(self, didFailWithError: error)
+ } else {
+ self.client?.urlProtocolDidFinishLoading(self)
+ }
+ }
+
+ task.resume()
+ }
+
+ override func stopLoading() {}
+}
+
+// MARK: - HTTPURLLogEntry
+struct HTTPURLLogEntry: Identifiable, CustomDebugStringConvertible {
+ let id: UUID
+ let date: Date
+ let request: URLRequest
+ let requestData: Data?
+ let response: HTTPURLResponse?
+ let responseData: Data?
+ let error: Error?
+
+ var debugDescription: String {
+ return "[HTTPURLLogEntry]\n"
+ + "πΉ RequestId: \(id.uuidString)\n"
+ + _requestDebugDescription
+ + "\n\n"
+ + _responseDebugDescription
+ }
+
+ var requestDebugDescription: String {
+ return "[HTTPURLLogEntry]\n"
+ + "πΉ RequestId: \(id.uuidString)\n"
+ + _requestDebugDescription
+ }
+
+ var responseDebugDescription: String {
+ return "[HTTPURLLogEntry]\n"
+ + "πΉ RequestId: \(id.uuidString)\n"
+ + _responseDebugDescription
+ }
+
+ private var _requestDebugDescription: String {
+ guard let requestHTTPMethod = request.httpMethod, let requestURL = request.url else {
+ return "βοΈ URLRequest is not complete"
+ }
+
+ var output = [String]()
+
+ output.append("β¬οΈ \(requestHTTPMethod) \(requestURL)")
+
+ if let httpHeaders = request.allHTTPHeaderFields?.sorted(by: { $0.key < $1.key }) {
+ output.append("πΈ Headers:")
+ output.append(contentsOf: httpHeaders.map({ "\t\($0): \($1)" }))
+ }
+
+ if let requestData {
+ output.append("πΉ Body:")
+ output.append("\t\(String(decoding: requestData, as: UTF8.self))")
+ }
+
+ return output.joined(separator: "\n")
+ }
+
+ private var _responseDebugDescription: String {
+ guard let response, let responseURL = response.url?.absoluteString else {
+ return "βοΈ URLResponse is not complete"
+ }
+
+ var output = [String]()
+
+ output.append("β¬οΈ \(response.statusCode) \(responseURL)")
+
+ if response.allHeaderFields.isEmpty == false, let httpHeaders = response.allHeaderFields as? [String: String] {
+ output.append("πΈ Headers:")
+ output.append(contentsOf: httpHeaders.sorted(by: { $0.key < $1.key }).map({ "\t\($0): \($1)" }))
+ }
+
+ if let responseData {
+ output.append("πΉ Body:")
+ output.append("\t\(String(decoding: responseData, as: UTF8.self))")
+ }
+
+ return output.joined(separator: "\n")
+ }
+}
+
+// MARK: - HTTPURLLogStore
+enum HTTPURLLogStore {
+ private static let queue = DispatchQueue(label: "network.log.store", attributes: .concurrent)
+ private static let kMaxCapacity: UInt = 20
+ private static var entries: [HTTPURLLogEntry] = []
+
+ fileprivate static func update(_ entry: HTTPURLLogEntry) {
+ queue.async(flags: .barrier) {
+ self.entries.append(entry)
+ if self.entries.count > self.kMaxCapacity {
+ self.entries.removeFirst()
+ }
+ }
+
+ NotificationCenter.default.post(name: .HTTPURLLogUpdated, object: nil, userInfo: ["data": entry])
+ }
+
+ static func all() -> [HTTPURLLogEntry] {
+ queue.sync { entries }
+ }
+
+ static func filter(_ predicate: (HTTPURLLogEntry) -> Bool) -> [HTTPURLLogEntry] {
+ queue.sync { entries.filter(predicate) }
+ }
+}
+
+extension Notification.Name {
+ static let HTTPURLLogUpdated = Notification.Name("HTTPURLLogUpdated")
+}
+
+// MARK: - Misc
+private func captureBody(from request: inout NSMutableURLRequest) -> Data? {
+ if let body = request.httpBody {
+ return body
+ }
+
+ if let stream = request.httpBodyStream {
+ let data = drainStream(stream)
+ request.httpBodyStream = InputStream(data: data)
+ return data
+ }
+
+ return nil
+}
+
+private func drainStream(_ stream: InputStream) -> Data {
+ stream.open()
+ defer { stream.close() }
+
+ var data = Data()
+ let bufferSize = 8 * 1024
+ var buffer = [UInt8](repeating: 0, count: bufferSize)
+
+ while true {
+ let bytesRead = stream.read(&buffer, maxLength: bufferSize)
+
+ if bytesRead > 0 {
+ data.append(buffer, count: bytesRead)
+ } else if bytesRead == 0 {
+ // End of stream
+ break
+ } else {
+ // Error occurred
+ break
+ }
+ }
+
+ return data
+}
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..540863e
--- /dev/null
+++ b/demo-ios-swift/demo-ios-swift/PrebidBannerViewController.swift
@@ -0,0 +1,234 @@
+//
+// PrebidBannerViewController.swift
+// demo-ios-swift
+//
+// Copyright Β© 2020 Optable Technologies Inc. All rights reserved.
+// See LICENSE for details.
+//
+
+import GoogleMobileAds
+import PrebidMobile
+import UIKit
+import OptableSDK
+
+private let AD_MANAGER_AD_UNIT_ID = "/21808260008/prebid_demo_app_original_api_banner"
+private let PREBID_STORED_IMP = "prebid-demo-banner-320-50"
+
+// MARK: - PrebidBannerViewController
+final class PrebidBannerViewController: UIViewController { // Outlets
+ @IBOutlet var adPlaceholder: UIView!
+ @IBOutlet var loadBannerButton: UIButton!
+ @IBOutlet var loadBannerFromCacheButton: UIButton!
+ @IBOutlet var clearTargetingCacheButton: UIButton!
+ @IBOutlet var targetingOutput: UITextView!
+
+ // PrebidMobile
+ private var pbmBannerAdUnit: BannerAdUnit!
+
+ // GoogleMobileAds - GAMBannerView
+ private var gamBannerView: AdManagerBannerView!
+
+ // Logging
+ private var targetingLog: String? { didSet { updateUILog() } }
+ private var witnessLog: String? { didSet { updateUILog() } }
+ private var profileLog: String? { didSet { updateUILog() } }
+ private var networkLogObserver: (any NSObjectProtocol)?
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ gamBannerView = AdManagerBannerView(adSize: AdSizeBanner)
+ gamBannerView.adUnitID = AD_MANAGER_AD_UNIT_ID
+ gamBannerView.rootViewController = self
+ gamBannerView.delegate = self
+ addBannerViewToView(gamBannerView)
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ startObservingNetworkLogs()
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ stopObservingNetworkLogs()
+ }
+
+ // MARK: Actions
+ @IBAction func loadBannerWithTargeting(_ sender: UIButton) {
+ do {
+ try OPTABLE!.targeting { [weak self] result in
+ switch result {
+ case let .success(optableTargeting):
+ print("[OptableSDK] β
Success on /targeting API call: \(optableTargeting)")
+ self?.loadBanner(optableTargeting)
+
+ case let .failure(error):
+ print("[OptableSDK] π« Error on /targeting API call: \(error)")
+ self?.loadBanner()
+ }
+ }
+ } catch {
+ print("[OptableSDK] π« Exception on /targeting API call: \(error)")
+ targetingLog = "π« EXCEPTION: \(error)"
+ }
+ }
+
+ @IBAction func loadBannerWithTargetingFromCache(_ sender: UIButton) {
+ if let cachedOptableTargeting = OPTABLE!.targetingFromCache() {
+ print("[OptableSDK] β
Cached targeting values found: \(cachedOptableTargeting)")
+ loadBanner(cachedOptableTargeting)
+ } else {
+ print("[OptableSDK] βΉοΈ Cache empty")
+ loadBanner()
+ }
+ }
+
+ @IBAction func clearTargetingCache(_ sender: UIButton) {
+ targetingOutput.text = "π§Ή Cleared local targeting cache.\n"
+ OPTABLE!.targetingClearCache()
+ }
+}
+
+// MARK: - Private
+private extension PrebidBannerViewController {
+ func loadBanner(_ optableTargeting: OptableTargeting? = nil) {
+ loadPrebidAd(optableTargeting)
+ witness()
+ profile()
+ }
+
+ func loadPrebidAd(_ optableTargeting: OptableTargeting? = nil) {
+ setOptableTargetingToPrebid(optableTargeting)
+
+ let adRequest = AdManagerRequest()
+ adRequest.customTargeting = optableTargeting?.gamTargetingKeywords as? [String: Any]
+
+ pbmBannerAdUnit = BannerAdUnit(configId: PREBID_STORED_IMP, size: .init(width: 320, height: 50))
+ pbmBannerAdUnit.fetchDemand(adObject: adRequest) { [weak self] status in
+ print("[PrebidMobile]:fetchDemand(adObject:): \(status.name())")
+ self?.loadGAMAd(adRequest)
+ }
+ }
+
+ func setOptableTargetingToPrebid(_ optableTargeting: OptableTargeting? = nil) {
+ guard
+ let optableTargeting,
+ let ortb2 = optableTargeting.ortb2
+ else {
+ PrebidMobile.Targeting.shared.setGlobalORTBConfig(nil)
+ return
+ }
+
+ PrebidMobile.Targeting.shared.setGlobalORTBConfig(ortb2)
+ }
+
+ func loadGAMAd(_ request: AdManagerRequest) {
+ gamBannerView.load(request)
+ }
+
+ func witness() {
+ do {
+ try OPTABLE!.witness(
+ event: "PrebidBannerViewController.loadBannerClicked",
+ properties: ["example": "value"]
+ ) { result in
+ switch result {
+ case .success:
+ print("[OptableSDK] β
Success on /witness API call")
+ case let .failure(error):
+ print("[OptableSDK] π« Error on /witness API call: \(error)")
+ }
+ }
+ } catch {
+ print("[OptableSDK] π« Exception on /witness API call: \(error)")
+ witnessLog = "π« EXCEPTION: \(error)"
+ }
+ }
+
+ func profile() {
+ do {
+ try OPTABLE!.profile(
+ traits: ["example": "value", "anotherExample": 123, "thirdExample": true]
+ ) { result in
+ switch result {
+ case .success:
+ print("[OptableSDK] β
Success on /profile API call")
+ case let .failure(error):
+ print("[OptableSDK] π« Error on /profile API call: \(error)")
+ }
+ }
+ } catch {
+ print("[OptableSDK] π« Exception on /profile API call: \(error)")
+ profileLog = "π« EXCEPTION: \(error)"
+ }
+ }
+}
+
+// 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("[PrebidBannerViewController] Failed to receive ad: \(error)")
+ }
+}
+
+// MARK: - Helpers
+private extension PrebidBannerViewController {
+ func addBannerViewToView(_ bannerView: AdManagerBannerView) {
+ bannerView.translatesAutoresizingMaskIntoConstraints = false
+ adPlaceholder.addSubview(bannerView)
+
+ NSLayoutConstraint.activate([
+ bannerView.centerXAnchor.constraint(equalTo: adPlaceholder.centerXAnchor),
+ bannerView.centerYAnchor.constraint(equalTo: adPlaceholder.centerYAnchor),
+ ])
+ }
+
+ func updateUILog() {
+ targetingOutput.text = [targetingLog, witnessLog, profileLog].compactMap({ $0 }).joined(separator: "\n\n")
+ }
+}
+
+// MARK: - Logging
+private extension PrebidBannerViewController {
+ /// Setups observation for URLRequest/URLResponse logging
+ func startObservingNetworkLogs() {
+ networkLogObserver = NotificationCenter.default
+ .addObserver(forName: .HTTPURLLogUpdated, object: nil, queue: .main, using: { [weak self] notification in
+ if let logEntry = notification.userInfo?["data"] as? HTTPURLLogEntry,
+ logEntry.request.url?.absoluteString.contains("/targeting") == true {
+ self?.targetingLog = logEntry.debugDescription
+ print(logEntry.response == nil ? logEntry.requestDebugDescription : logEntry.responseDebugDescription)
+ }
+
+ if let logEntry = notification.userInfo?["data"] as? HTTPURLLogEntry,
+ logEntry.request.url?.absoluteString.contains("/witness") == true {
+ self?.witnessLog = logEntry.debugDescription
+ print(logEntry.response == nil ? logEntry.requestDebugDescription : logEntry.responseDebugDescription)
+ }
+
+ if let logEntry = notification.userInfo?["data"] as? HTTPURLLogEntry,
+ logEntry.request.url?.absoluteString.contains("/profile") == true {
+ self?.profileLog = logEntry.debugDescription
+ print(logEntry.response == nil ? logEntry.requestDebugDescription : logEntry.responseDebugDescription)
+ }
+ })
+ }
+
+ func stopObservingNetworkLogs() {
+ guard let networkLogObserver else { return }
+ NotificationCenter.default.removeObserver(networkLogObserver)
+ }
+}
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()
- }
- }
- }
-}
diff --git a/docs/identify-from-url.md b/docs/identify-from-url.md
new file mode 100644
index 0000000..465c4ff
--- /dev/null
+++ b/docs/identify-from-url.md
@@ -0,0 +1,47 @@
+## 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];
+ }
+ ...
+}
+```
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)