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.xcodeproj/project.pbxproj b/OptableSDK.xcodeproj/project.pbxproj
index 05287e6..1483d30 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;
};
@@ -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..77fd81b 100644
--- a/Package.swift
+++ b/Package.swift
@@ -23,7 +23,7 @@ let package = Package(
name: "OptableSDK",
dependencies: [],
path: "Source",
- exclude: ["Source/Info.plist"]),
+ exclude: ["Info.plist"]),
.testTarget(
name: "OptableSDKTests",
dependencies: ["OptableSDK"],
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/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..172dc05
--- /dev/null
+++ b/Source/Core/EdgeAPI.swift
@@ -0,0 +1,247 @@
+//
+// EdgeAPI.swift
+// OptableSDK
+//
+// Created by user on 15.12.2025.
+// Copyright © 2025 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..8c1ee0f 100644
--- a/Source/Core/LocalStorage.swift
+++ b/Source/Core/LocalStorage.swift
@@ -2,33 +2,36 @@
// 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)
}
@@ -36,11 +39,11 @@ class LocalStorage: NSObject {
return UserDefaults.standard.dictionary(forKey: targetingKey)
}
- func setTargeting(_ keyvalues: [String: Any]) -> Void {
+ func setTargeting(_ keyvalues: [String: Any]) {
UserDefaults.standard.setValue(keyvalues, forKey: targetingKey)
}
- func clearTargeting() -> Void {
+ func clearTargeting() {
UserDefaults.standard.removeObject(forKey: targetingKey)
}
}
diff --git a/Source/Core/OptableError.swift b/Source/Core/OptableError.swift
new file mode 100644
index 0000000..ac6b666
--- /dev/null
+++ b/Source/Core/OptableError.swift
@@ -0,0 +1,27 @@
+//
+// OptableError.swift
+// OptableSDK
+//
+// Created by user on 16.12.2025.
+// Copyright © 2025 Optable Technologies, Inc. All rights reserved.
+//
+
+import Foundation
+
+enum OptableError {
+ static func identify(_ message: String, code: Int = -1) -> NSError {
+ NSError(domain: "OptableSDK.identify", code: code, userInfo: [NSLocalizedDescriptionKey: message])
+ }
+
+ static func profile(_ message: String, code: Int = -1) -> NSError {
+ NSError(domain: "OptableSDK.profile", code: code, userInfo: [NSLocalizedDescriptionKey: message])
+ }
+
+ static func targeting(_ message: String, code: Int = -1) -> NSError {
+ NSError(domain: "OptableSDK.targeting", code: code, userInfo: [NSLocalizedDescriptionKey: message])
+ }
+
+ static func witness(_ message: String, code: Int = -1) -> NSError {
+ NSError(domain: "OptableSDK.witness", code: code, userInfo: [NSLocalizedDescriptionKey: message])
+ }
+}
diff --git a/Source/Core/OptableIdentifierEncoder.swift b/Source/Core/OptableIdentifierEncoder.swift
new file mode 100644
index 0000000..fef5d77
--- /dev/null
+++ b/Source/Core/OptableIdentifierEncoder.swift
@@ -0,0 +1,184 @@
+//
+// OptableIdentifierEncoder.swift
+// OptableSDK
+//
+// Created by user on 16.12.2025.
+// Copyright © 2025 Optable Technologies, Inc. All rights reserved.
+//
+
+import CommonCrypto
+import Foundation
+#if canImport(CryptoKit)
+ import CryptoKit
+#endif
+
+// MARK: - OptableIdentifierEncoder
+enum OptableIdentifierEncoder {
+ /// Builds Enriched 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 Enriched 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 Enriched Identifier from Postal code
+ static func postalCode(_ postalCode: String) -> String {
+ let prefix = OptableIdentifierType.postalCode.rawValue
+ let identifier = postalCode.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Enriched Identifier from IPv4 address
+ static func ipv4(_ ipv4: String) -> String {
+ let prefix = OptableIdentifierType.ipv4Address.rawValue
+ let identifier = ipv4.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Enriched Identifier from IPv6 address
+ static func ipv6(_ ipv6: String) -> String {
+ let prefix = OptableIdentifierType.ipv6Address.rawValue
+ let identifier = ipv6.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Enriched Identifier from Apple IDFA
+ static func idfa(_ idfa: String) -> String {
+ let prefix = OptableIdentifierType.appleIDFA.rawValue
+ let identifier = idfa.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Enriched Identifier from Google GAID
+ static func gaid(_ gaid: String) -> String {
+ let prefix = OptableIdentifierType.googleGAID.rawValue
+ let identifier = gaid.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Enriched Identifier from Roku RIDA
+ static func rida(_ rida: String) -> String {
+ let prefix = OptableIdentifierType.rokuRIDA.rawValue
+ let identifier = rida.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Enriched Identifier from Samsung TV TIFA
+ static func tifa(_ tifa: String) -> String {
+ let prefix = OptableIdentifierType.samsungTIFA.rawValue
+ let identifier = tifa.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Enriched Identifier from Amazon Fire AFAI
+ static func afai(_ afai: String) -> String {
+ let prefix = OptableIdentifierType.amazonFireAFAI.rawValue
+ let identifier = afai.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Enriched Identifier from NetID
+ static func netid(_ netid: String) -> String {
+ let prefix = OptableIdentifierType.netID.rawValue
+ let identifier = netid.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Enriched Identifier from ID5
+ static func id5(_ id5: String) -> String {
+ let prefix = OptableIdentifierType.id5.rawValue
+ let identifier = id5.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Enriched Identifier from Utiq
+ static func utiq(_ utiq: String) -> String {
+ let prefix = OptableIdentifierType.utiq.rawValue
+ let identifier = utiq.components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().lowercased()
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Enriched Identifier from Custom Publisher Provided ID (PPID)
+ static func custom(idx: Int = 0, _ ppid: String) -> String {
+ let prefix = OptableIdentifierType.custom(idx).rawValue
+ let identifier = ppid.trimmingCharacters(in: .whitespacesAndNewlines)
+ return "\(prefix):\(identifier)"
+ }
+
+ /// Builds Enriched Identifier from Optable Visitor ID (VID)
+ static func vid(_ vid: String) -> String {
+ let prefix = 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..c49f5d8
--- /dev/null
+++ b/Source/Misc/AppTrackingTransparency.swift
@@ -0,0 +1,88 @@
+//
+// AppTrackingTransparency.swift
+// OptableSDK
+//
+// Created by user on 15.12.2025.
+// Copyright © 2025 Optable Technologies, Inc. All rights reserved.
+//
+
+#if canImport(AdSupport)
+
+ import AdSupport
+
+ #if canImport(AppTrackingTransparency)
+ import AppTrackingTransparency
+ #endif
+
+ import Foundation
+
+ enum ATT {
+ static var advertisingIdentifier: UUID {
+ ASIdentifierManager.shared().advertisingIdentifier
+ }
+
+ @available(iOS, introduced: 6, deprecated: 14, message: "This has been replaced by functionality in AppTrackingTransparency's ATTrackingManager class.")
+ static var isAdvertisingTrackingEnabled: Bool {
+ ASIdentifierManager.shared().isAdvertisingTrackingEnabled
+ }
+
+ static var 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..8f1eb64
--- /dev/null
+++ b/Source/Misc/IABConsent.swift
@@ -0,0 +1,37 @@
+//
+// IABConsent.swift
+// OptableSDK
+//
+// Created by user on 19.12.2025.
+// Copyright © 2025 Optable Technologies, Inc. All rights reserved.
+//
+
+import Foundation
+
+/**
+ IABConsent is responsible retrieving user consent according to the IAB Transparency & Consent Framework
+
+ For more info check: [](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md)
+ */
+enum IABConsent {
+ enum Keys {
+ static let IABTCF_TCString = "IABTCF_TCString"
+ static let IABTCF_gdprApplies = "IABTCF_gdprApplies"
+ static let IABGPP_2_TCString = "IABGPP_2_TCString"
+ }
+
+ static var gdprApplies: Bool? {
+ if let iabValue = UserDefaults.standard.string(forKey: Keys.IABTCF_gdprApplies) {
+ return NSString(string: iabValue).boolValue
+ }
+ return nil
+ }
+
+ static var gdprTC: String? {
+ UserDefaults.standard.string(forKey: Keys.IABTCF_TCString)
+ }
+
+ static var gppTC: String? {
+ UserDefaults.standard.string(forKey: Keys.IABGPP_2_TCString)
+ }
+}
diff --git a/Source/Misc/Networking.swift b/Source/Misc/Networking.swift
new file mode 100644
index 0000000..a1c598e
--- /dev/null
+++ b/Source/Misc/Networking.swift
@@ -0,0 +1,178 @@
+//
+// Networking.swift
+// OptableSDK
+//
+// Created by user on 15.12.2025.
+// Copyright © 2025 Optable Technologies, Inc. All rights reserved.
+//
+
+import Foundation
+
+// MARK: - HTTPMethod
+enum HTTPMethod: String {
+ case GET
+ case HEAD
+ case POST
+ case PUT
+ case DELETE
+ case CONNECT
+ case OPTIONS
+ case TRACE
+ case PATCH
+}
+
+// MARK: - HTTPHeader
+enum HTTPHeader: String {
+ // Content negotiation
+ case accept = "Accept"
+ case contentType = "Content-Type"
+ case contentLength = "Content-Length"
+ case contentEncoding = "Content-Encoding"
+
+ // Authorization / security
+ case authorization = "Authorization"
+ case wwwAuthenticate = "WWW-Authenticate"
+ case proxyAuthorization = "Proxy-Authorization"
+
+ // Caching
+ case cacheControl = "Cache-Control"
+ case pragma = "Pragma"
+ case expires = "Expires"
+ case etag = "ETag"
+ case ifNoneMatch = "If-None-Match"
+ case ifModifiedSince = "If-Modified-Since"
+
+ // Connection
+ case connection = "Connection"
+ case keepAlive = "Keep-Alive"
+ case upgrade = "Upgrade"
+
+ // User / client info
+ case userAgent = "User-Agent"
+ case referer = "Referer"
+ case origin = "Origin"
+ case host = "Host"
+
+ // Cookies
+ case cookie = "Cookie"
+ case setCookie = "Set-Cookie"
+
+ // Range / transfer
+ case range = "Range"
+ case acceptRanges = "Accept-Ranges"
+ case transferEncoding = "Transfer-Encoding"
+
+ // CORS
+ case accessControlAllowOrigin = "Access-Control-Allow-Origin"
+ case accessControlAllowMethods = "Access-Control-Allow-Methods"
+ case accessControlAllowHeaders = "Access-Control-Allow-Headers"
+ case accessControlExposeHeaders = "Access-Control-Expose-Headers"
+ case accessControlAllowCredentials = "Access-Control-Allow-Credentials"
+ case accessControlMaxAge = "Access-Control-Max-Age"
+
+ // Compression
+ case acceptEncoding = "Accept-Encoding"
+ case acceptLanguage = "Accept-Language"
+}
+
+// MARK: - HTTPHeaders
+struct HTTPHeaders {
+ private var dict: [String: String] = [:]
+
+ var asDict: [String: String] { dict }
+
+ init() {}
+
+ init(_ dict: [String: String]) {
+ self.dict = dict
+ }
+
+ subscript(_ key: HTTPHeader) -> String? {
+ get { dict[key.rawValue] }
+ set { dict[key.rawValue] = newValue }
+ }
+
+ subscript(_ key: String) -> String? {
+ get { dict[key] }
+ set { dict[key] = newValue }
+ }
+}
+
+// MARK: - HTTPQuery
+enum HTTPQuery {
+ case jsonObject(Encodable)
+ case dict([String: String?])
+}
+
+// MARK: - HTTPBody
+enum HTTPBody {
+ case jsonObject(Encodable)
+ case jsonArray([Any])
+ case jsonDict([String: Any])
+}
+
+// MARK: - HTTPStatusCode
+enum HTTPStatusCode: 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..1f323b0
--- /dev/null
+++ b/Source/Misc/URL+Compat.swift
@@ -0,0 +1,22 @@
+//
+// URL+Compat.swift
+// OptableSDK
+//
+// Created by user on 19.12.2025.
+// Copyright © 2025 Optable Technologies, Inc. All rights reserved.
+//
+
+import Foundation
+
+extension URL {
+ mutating func compatAppend(queryItems: [URLQueryItem]) {
+ if #available(iOS 16.0, *) {
+ append(queryItems: queryItems)
+ } else {
+ guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { return }
+ components.queryItems?.append(contentsOf: queryItems)
+ guard let url = components.url else { return }
+ self = url
+ }
+ }
+}
diff --git a/Source/OptableConfig.swift b/Source/OptableConfig.swift
new file mode 100644
index 0000000..a4ca881
--- /dev/null
+++ b/Source/OptableConfig.swift
@@ -0,0 +1,136 @@
+//
+// 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?
+
+ // TODO: timeout per-request?
+ /// Timeout for requests in the form of `{{timeout}}ms`. This timeout will override all other timeouts.
+// @objc
+// public var timeout: TimeInterval = 0
+
+ /// The Optable passport JWT.
+// @objc
+// public var passport: String?
+
+ // MARK: Inits
+ /**
+ - Parameters:
+ - tenant: The tenant name associated with the configuration. E.g. `acmeco.optable.co` => `acmeco`.
+ - 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..1e97187
--- /dev/null
+++ b/Source/OptableIdentifierType.swift
@@ -0,0 +1,93 @@
+//
+// OptableIdentifierType.swift
+// OptableSDK
+//
+// Created by user on 16.12.2025.
+// Copyright © 2025 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..ac11d72
--- /dev/null
+++ b/Source/OptableIdentifiers.swift
@@ -0,0 +1,128 @@
+//
+// OptableIdentifiers.swift
+// OptableSDK
+//
+// Created by user on 15.12.2025.
+// Copyright © 2025 Optable Technologies, Inc. All rights reserved.
+//
+
+import Foundation
+
+// MARK: - OptableIdentifiers
+/**
+ Optable Identifiers container
+
+ For more info check:
+ [](https://docs.optable.co/optable-documentation/getting-started/reference/identifier-types)
+
+ */
+public struct OptableIdentifiers {
+ public var dict: [String: String] = [:]
+
+ public init(
+ emailAddress: String? = nil,
+ phoneNumber: String? = nil,
+ postalCode: String? = nil,
+ ipv4Address: String? = nil,
+ ipv6Address: String? = nil,
+ appleIDFA: String? = nil,
+ googleGAID: String? = nil,
+ rokuRIDA: String? = nil,
+ samsungTIFA: String? = nil,
+ amazonFireAFAI: String? = nil,
+ netID: String? = nil,
+ id5: String? = nil,
+ utiq: String? = nil,
+ custom: [String: String]? = nil
+ ) {
+ self.dict[OptableIdentifierType.emailAddress.rawValue] = emailAddress
+ self.dict[OptableIdentifierType.phoneNumber.rawValue] = phoneNumber
+ self.dict[OptableIdentifierType.postalCode.rawValue] = postalCode
+ self.dict[OptableIdentifierType.ipv4Address.rawValue] = ipv4Address
+ self.dict[OptableIdentifierType.ipv6Address.rawValue] = ipv6Address
+ self.dict[OptableIdentifierType.appleIDFA.rawValue] = appleIDFA
+ self.dict[OptableIdentifierType.googleGAID.rawValue] = googleGAID
+ self.dict[OptableIdentifierType.rokuRIDA.rawValue] = rokuRIDA
+ self.dict[OptableIdentifierType.samsungTIFA.rawValue] = samsungTIFA
+ self.dict[OptableIdentifierType.amazonFireAFAI.rawValue] = amazonFireAFAI
+ self.dict[OptableIdentifierType.netID.rawValue] = netID
+ self.dict[OptableIdentifierType.id5.rawValue] = id5
+ self.dict[OptableIdentifierType.utiq.rawValue] = utiq
+ self.dict.merge(custom ?? [:], uniquingKeysWith: { _, new in new })
+ }
+
+ public init(_ dict: [String: String] = [:]) {
+ self.dict = dict
+ }
+
+ public subscript(_ key: String) -> String? {
+ get { dict[key] }
+ set { dict[key] = newValue }
+ }
+
+ 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 enrichedIds = generateEnrichedIds()
+
+ var container = encoder.unkeyedContainer()
+ for eid in enrichedIds {
+ try container.encode(eid)
+ }
+ }
+}
diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift
index be2ec69..cc955b5 100644
--- a/Source/OptableSDK.swift
+++ b/Source/OptableSDK.swift
@@ -7,26 +7,20 @@
//
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)
@@ -39,151 +33,326 @@ public protocol OptableDelegate {
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?
- public enum OptableError: Error {
- case identify(String)
- case profile(String)
- case targeting(String)
- case witness(String)
+ let config: OptableConfig
+ let api: EdgeAPI
+
+ /// `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.
+
+ The targeting method is asynchronous, and on completion it will call the specified completion handler,
+ passing it either the NSDictionary targeting data on success, or an NSError on failure.
+
+ On success, this method will also cache the resulting targeting data in client storage, which can
+ be access using targetingFromCache(), and cleared using targetingClearCache().
+ */
+ func targeting(ids: [String]? = nil, completion: @escaping (Result) -> Void) throws {
+ try _targeting(ids: ids, completion: completion)
+ }
- if (email != "") {
- ids.append(self.eid(email))
+ /// targetingFromCache() returns the previously cached targeting data, if any.
+ @objc
+ func targetingFromCache() -> NSDictionary? {
+ guard let keyvalues = self.api.storage.getTargeting() as NSDictionary? else {
+ return nil
}
+ return keyvalues
+ }
- 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))
- }
+ /// 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 -> NSDictionary {
+ 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.
- try self.identify(ids: ids, completion)
+ 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(keyvalues):
+ self.delegate?.targetingOk(keyvalues)
+ case let .failure(error as NSError):
+ self.delegate?.targetingErr(error)
+ }
+ })
}
+}
- ///
- /// identify(email, aaid, ppid) is the "delegate variant" of the identify(email, aaid, ppid, completion) method.
- /// It wraps the latter with a delegator callback.
- ///
- /// This is the Objective-C compatible version of the identify(email, aaid, ppid, completion) API.
- ///
+// MARK: - Witness
+public extension OptableSDK {
+ /**
+ witness(event, properties, completion) calls the Optable Sandbox "witness" API in order to log a specified 'event' (e.g., "app.screenView", "ui.buttonPressed"), with the specified keyvalue NSDictionary 'properties', which can be subsequently used for audience assembly.
+
+ The witness method is asynchronous, and on completion it will call the specified completion handler,
+ passing it either the HTTPURLResponse on success, or an NSError on failure.
+ */
+ func witness(event: String, properties: NSDictionary, _ completion: @escaping (Result) -> Void) throws {
+ try _witness(event: event, properties: properties, completion: completion)
+ }
+
+ // MARK: Async/Await support
+ /**
+ This is the Swift Concurrency compatible version of the `witness(event, properties, completion)` API.
+
+ Instead of completion callbacks, function have to be awaited.
+ */
+ @available(iOS 13.0, *)
+ func witness(event: String, properties: NSDictionary) async throws -> HTTPURLResponse {
+ return try await withCheckedThrowingContinuation({ [unowned self] continuation in
+ do {
+ try self._witness(event: event, properties: properties, completion: { continuation.resume(with: $0) })
+ } catch {
+ continuation.resume(throwing: error)
+ }
+ })
+ }
+
+ // MARK: Objective-C support
+ /**
+ This is the Objective-C compatible version of the `witness(event, properties, completion)` API.
+
+ Instead of completion callbacks, delegate methods are called.
+ */
@objc
- 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 {
+ private 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()
+ }
+
+ private func _targeting(ids: [String]?, completion: @escaping (Result) -> Void) throws {
+ guard let request = try api.targeting(ids: ids) else {
+ throw OptableError.targeting("Failed to create targeting request")
+ }
+
+ api.dispatch(request: request, completionHandler: { data, response, error in
guard let response = response as? HTTPURLResponse, error == nil, data != nil else {
if let err = error {
completion(.failure(OptableError.targeting("Session error: \(err)")))
@@ -192,13 +361,9 @@ 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
}
@@ -207,62 +372,21 @@ public class OptableSDK: NSObject {
let result = keyvalues as? NSDictionary ?? NSDictionary()
/// We cache the latest targeting result in client storage for targetingFromCache() users:
- self.client.storage.setTargeting(keyvalues as? [String: Any] ?? [String: Any]())
+ self.api.storage.setTargeting(keyvalues as? [String: Any] ?? [String: Any]())
completion(.success(result))
} catch {
completion(.failure(OptableError.targeting("Error parsing JSON response: \(error)")))
}
- }?.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)
- }
- }
+ }).resume()
}
- ///
- /// 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 +395,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 +418,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.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)
- }
- }
- }
-
- ///
- /// 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
- }
-
- @objc
- private func cchash(_ input: Data) -> String {
- var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
- input.withUnsafeBytes { bytes in
- _ = CC_SHA256(bytes.baseAddress, CC_LONG(input.count), &digest)
- }
- return digest.makeIterator().compactMap {
- String(format: "%02x", $0)
- }.joined()
- }
-
- ///
- /// aaid(idfa) is a helper that returns the type-prefixed Apple ID For Advertising
- ///
- @objc
- public func aaid(_ idfa: String) -> String {
- return "a:" + idfa.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
- }
-
- ///
- /// cid(ppid) is a helper that returns custom type-prefixed origin-provided PPID
- ///
- @objc
- public func cid(_ ppid: String) -> String {
- return "c:" + ppid.trimmingCharacters(in: .whitespacesAndNewlines)
+ }).resume()
}
- ///
- /// 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
- }
- if qi.name.lowercased() == "oeid" {
- oeid = val
- break
- }
- }
-
- /// Check that oeid looks like a valid SHA256:
- let range = NSRange(location: 0, length: oeid.utf16.count)
- guard let regex = try? NSRegularExpression(pattern: "[a-f0-9]{64}", options: .caseInsensitive) else { return "" }
- if (oeid.count != 64) || (regex.firstMatch(in: oeid, options: [], range: range) == nil) {
- return ""
- }
-
- return "e:" + oeid.lowercased()
- }
-
- ///
- /// tryIdentifyFromURL(urlString) is a helper that attempts to find a valid-looking
- /// "oeid" parameter in the specified urlString's query string parameters and, if found,
- /// calls self.identify([oeid]).
- ///
- /// The use for this is when handling incoming universal links which might contain an
- /// "oeid" value with the SHA256(downcase(email)) of an incoming user, such as encoded
- /// links in newsletter Emails sent by the application developer.
- ///
- @objc
- 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
+ private static func generateEdgeAPIErrorDescription(with data: Data?, response: HTTPURLResponse) -> String {
+ var msg = "HTTP response.statusCode: \(response.statusCode)"
+ do {
+ let json = try JSONSerialization.jsonObject(with: data ?? Data(), options: [])
+ msg += ", data: \(json)"
+ } catch {}
+ return msg
}
}
diff --git a/Tests/Integration/OptableSDKTests.swift b/Tests/Integration/OptableSDKTests.swift
new file mode 100644
index 0000000..1c8e065
--- /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: NSDictionary = try await sdk.targeting()
+ XCTAssert(response.allKeys.isEmpty == false)
+ }
+
+ func test_target_callback() throws {
+ let expectation = expectation(description: "target-callback-expectation")
+ try sdk.targeting(completion: { result in
+ switch result {
+ case let .success(response):
+ XCTAssert(response.allKeys.isEmpty == false)
+ case let .failure(failure):
+ XCTFail("Expected success, got error: \(failure)")
+ }
+ expectation.fulfill()
+ })
+ wait(for: [expectation], timeout: 10)
+ }
+
+ func test_target_delegate() throws {
+ try sdk.targeting()
+ wait(for: [targetExpectation], timeout: 10)
+ }
+
+ // MARK: Witness
+ @available(iOS 13.0, *)
+ func test_witness_async() async throws {
+ let response: HTTPURLResponse = try await sdk.witness(event: "test", properties: ["integration-test-witness": "integration-test-witness-value"])
+ XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor"))
+ XCTAssert(response.statusCode == 200)
+ }
+
+ func test_witness_callbacks() throws {
+ let expectation = expectation(description: "witness-callback-expectation")
+ try sdk.witness(event: "test", properties: ["integration-test-witness": "integration-test-witness-value"], { result in
+ switch result {
+ case let .success(response):
+ XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor"))
+ XCTAssert(response.statusCode == 200)
+ case let .failure(failure):
+ XCTFail("Expected success, got error: \(failure)")
+ }
+ expectation.fulfill()
+ })
+ wait(for: [expectation], timeout: 10)
+ }
+
+ func test_witness_delegate() throws {
+ try sdk.witness(event: "test", properties: ["integration-test-witness": "integration-test-witness-value"])
+ wait(for: [witnessExpectation], timeout: 10)
+ }
+
+ // MARK: Profile
+ @available(iOS 13.0, *)
+ func test_profile_async() async throws {
+ let response: HTTPURLResponse = try await sdk.profile(traits: ["integration-test-profile": "integration-test-profile-value"])
+ XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor"))
+ XCTAssert(response.statusCode == 200)
+ }
+
+ func test_profile_callbacks() throws {
+ let expectation = expectation(description: "profile-callback-expectation")
+ try sdk.profile(traits: ["integration-test-profile": "integration-test-profile-value"], { result in
+ switch result {
+ case let .success(response):
+ XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor"))
+ XCTAssert(response.statusCode == 200)
+ case let .failure(failure):
+ XCTFail("Expected success, got error: \(failure)")
+ }
+ expectation.fulfill()
+ })
+ wait(for: [expectation], timeout: 10)
+ }
+
+ func test_profile_delegate() throws {
+ try sdk.profile(traits: ["integration-test-profile": "integration-test-profile-value"])
+ wait(for: [profileExpectation], timeout: 10)
+ }
+}
+
+// MARK: - OptableDelegate
+extension OptableSDKTests: OptableDelegate {
+ func identifyOk(_ result: HTTPURLResponse) {
+ XCTAssert(result.allHeaderFields.keys.contains("x-optable-visitor"))
+ XCTAssert(result.statusCode == 200)
+ identifyExpectation.fulfill()
+ }
+
+ func identifyErr(_ error: NSError) {
+ XCTFail("Expected success, got error: \(error)")
+ identifyExpectation.fulfill()
+ }
+
+ func profileOk(_ result: HTTPURLResponse) {
+ XCTAssert(result.allHeaderFields.keys.contains("x-optable-visitor"))
+ XCTAssert(result.statusCode == 200)
+ profileExpectation.fulfill()
+ }
+
+ func profileErr(_ error: NSError) {
+ XCTFail("Expected success, got error: \(error)")
+ profileExpectation.fulfill()
+ }
+
+ func targetingOk(_ result: NSDictionary) {
+ XCTAssert(result.allKeys.isEmpty == false)
+ targetExpectation.fulfill()
+ }
+
+ func targetingErr(_ error: NSError) {
+ XCTFail("Expected success, got error: \(error)")
+ targetExpectation.fulfill()
+ }
+
+ func witnessOk(_ result: HTTPURLResponse) {
+ XCTAssert(result.allHeaderFields.keys.contains("x-optable-visitor"))
+ XCTAssert(result.statusCode == 200)
+ witnessExpectation.fulfill()
+ }
+
+ func witnessErr(_ error: NSError) {
+ XCTFail("Expected success, got error: \(error)")
+ witnessExpectation.fulfill()
+ }
+}
diff --git a/Tests/Misc/Constants.swift b/Tests/Misc/Constants.swift
new file mode 100644
index 0000000..5e6fd4c
--- /dev/null
+++ b/Tests/Misc/Constants.swift
@@ -0,0 +1,55 @@
+//
+// Constants.swift
+// OptableSDK
+//
+// Created by user on 19.12.2025.
+// Copyright © 2025 Optable Technologies, Inc. All rights reserved.
+//
+
+import Foundation
+
+enum T {
+ enum api {
+ enum host {
+ static let na: String = "na.edge.optable.co"
+ static let au: String = "au.edge.optable.co"
+
+ static let all: [String] = [na, au]
+ }
+
+ enum endpoint {
+ static let identify: String = "identify"
+ static let target: String = "target"
+ static let witness: String = "witness"
+ static let profile: String = "profile"
+
+ static let all: [String] = [identify, target, witness, profile]
+ }
+
+ enum path {
+ static let v1: String = "v1"
+ static let v2: String = "v2"
+
+ static let all: [String] = [v1, v2]
+ }
+
+ enum tenant {
+ static let prebidtest: String = "prebidtest"
+ static let test: String = "test-tenant"
+
+ static let all: [String] = [prebidtest, test]
+ }
+
+ enum slug {
+ static let iosSDK: String = "ios-sdk"
+ static let jsSDK: String = "js-sdk"
+
+ static let all: [String] = [iosSDK, jsSDK]
+ }
+
+ static let userAgent: String = "ios-integration-tests"
+
+ static let apiKey: String = "test-api-key"
+ static let apiKeyBearer: String = "Bearer \(apiKey)"
+ }
+}
diff --git a/Tests/Misc/cartesianProduct.swift b/Tests/Misc/cartesianProduct.swift
new file mode 100644
index 0000000..c1049d7
--- /dev/null
+++ b/Tests/Misc/cartesianProduct.swift
@@ -0,0 +1,22 @@
+//
+// XCTAssertEqual.swift
+// OptableSDK
+//
+// Created by user on 17.12.2025.
+// Copyright © 2025 Optable Technologies, Inc. All rights reserved.
+//
+
+import Foundation
+
+/// Returns the Cartesian product of multiple arrays
+/// - Parameter arrays: An array of arrays of type T
+/// - Returns: Array of arrays representing all combinations
+func cartesianProduct(_ arrays: [[T]]) -> [[T]] {
+ arrays.reduce([[]] as [[T]]) { acc, array in
+ acc.flatMap { prefix in
+ array.map { element in
+ prefix + [element]
+ }
+ }
+ }
+}
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..fa5b41f
--- /dev/null
+++ b/Tests/Unit/EdgeAPITests.swift
@@ -0,0 +1,243 @@
+//
+// EdgeAPITests.swift
+// OptableSDK
+//
+// Created by user on 17.12.2025.
+// Copyright © 2025 Optable Technologies, Inc. All rights reserved.
+//
+
+@testable import OptableSDK
+import XCTest
+
+class EdgeAPITests: XCTestCase {
+ 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..07e3c68
--- /dev/null
+++ b/Tests/Unit/OptableIdentifierEncoderTests.swift
@@ -0,0 +1,160 @@
+//
+// OptableIdentifierEncoderTests.swift
+// OptableSDK
+//
+// Created by user on 17.12.2025.
+// Copyright © 2025 Optable Technologies, Inc. All rights reserved.
+//
+
+@testable import OptableSDK
+import XCTest
+
+class OptableIdentifierEncoderTests: XCTestCase {
+ typealias SUT = OptableIdentifierEncoder
+
+ func test_email() throws {
+ var expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"
+ XCTAssertEqual(expected, SUT.email("123"))
+ XCTAssertEqual(expected, SUT.email(" 123"))
+ XCTAssertEqual(expected, SUT.email("123 "))
+ XCTAssertEqual(expected, SUT.email(" 123 "))
+
+ expected = "e:9e9bff5609b2e4b721e682ce7a0759d4f042819bc15a698bcb99db7897555239"
+ XCTAssertEqual(expected, SUT.email("tEsT@ FooBarBaz.CoM"))
+ XCTAssertEqual(expected, SUT.email(" test@foobarbaz.com"))
+ XCTAssertEqual(expected, SUT.email("TEST@FOOBARBAZ.COM "))
+ XCTAssertEqual(expected, SUT.email("TeSt@ f O O b A R b A Z.cOm"))
+ }
+
+ func test_phoneNumber() throws {
+ let expected = "p:ebad3b64ae96005048fca1af2f15e5251ad3844d00fb80252711de9b651c8e46"
+ XCTAssertEqual(expected, SUT.phoneNumber("+33 555 456789"))
+ XCTAssertEqual(expected, SUT.phoneNumber("+33555456789"))
+ XCTAssertEqual(expected, SUT.phoneNumber("+3 3 5 5 5456789"))
+ XCTAssertEqual(expected, SUT.phoneNumber(" +33555456789 "))
+ }
+
+ func test_postalCode() throws {
+ XCTAssertEqual("z:m5v3l9", SUT.postalCode(" M5V 3L9"))
+ XCTAssertEqual("z:t2p5h1", SUT.postalCode("T 2 P 5 H 1"))
+ XCTAssertEqual("z:90210", SUT.postalCode("90210"))
+ XCTAssertEqual("z:10001", SUT.postalCode("10001"))
+ XCTAssertEqual("z:sw1a1aa", SUT.postalCode("SW1A 1AA"))
+ XCTAssertEqual("z:eh11bb", SUT.postalCode("EH1 1BB"))
+ }
+
+ func test_id5() throws {
+ let expected = "id5:ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg"
+ XCTAssertEqual(expected, SUT.id5(" ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg "))
+ XCTAssertEqual(expected, SUT.id5("ID5*UDWnp 3JOtWV0ky-bHvE eU4xOVHXCmYeg2 4YigF8iAymU HplfYSEl M3fy79h8p-Fg"))
+ }
+
+ func test_utiq() throws {
+ let expected = "utiq:496f5db5-681f-4392-acd5-0d4f6e2f6b88"
+ XCTAssertEqual(expected, SUT.utiq("496f5DB5-681F-4392-aCD5-0d4f6e2f6b88"))
+ XCTAssertEqual(expected, SUT.utiq(" 496f5db5 -681f -4392- acd5-0d4f6e2f6b88 "))
+ }
+
+ func test_ipv4() throws {
+ let expected = "i4:8.8.8.8"
+ XCTAssertEqual(expected, SUT.ipv4("8.8.8.8"))
+ XCTAssertEqual(expected, SUT.ipv4(" 8. 8. 8. 8 "))
+ }
+
+ func test_ipv6() throws {
+ let expected = "i6:2001:0db8:85a3:0000:0000:8a2e:0370:7334"
+ XCTAssertEqual(expected, SUT.ipv6("2001:0DB8:85A3:0000:0000:8a2e:0370:7334"))
+ XCTAssertEqual(expected, SUT.ipv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334"))
+ }
+
+ func test_idfa() throws {
+ let expected = "a:496f5db5-681f-4392-acd5-0d4f6e2f6b88"
+ XCTAssertEqual(expected, SUT.idfa("496f5DB5-681F-4392-acd5-0d4f6e2f6b88"))
+ XCTAssertEqual(expected, SUT.idfa("496f5db5- 681f- 4392- acd5- 0d4f6e2f6b88"))
+ }
+
+ func test_gaid() throws {
+ let expected = "g:64873d9f-d5af-4770-8bcb-167a220eb17d"
+ XCTAssertEqual(expected, SUT.gaid("64873d9f-d5AF-4770-8bcb-167a220eb17d"))
+ XCTAssertEqual(expected, SUT.gaid(" 64873d9f- d5af-4770- 8bcb-167a220eb17d "))
+ }
+
+ func test_rida() throws {
+ let expected = "r:0b179df0-6cd5-49f1-be21-425d002e0d22"
+ XCTAssertEqual(expected, SUT.rida("0b179df0-6CD5-49f1-be21-425d002e0d22"))
+ XCTAssertEqual(expected, SUT.rida(" 0b179df0 -6cd5- 49f1-be21-425d002e0d22 "))
+ }
+
+ func test_tifa() throws {
+ let expected = "s:e0ef86a8-6ebf-4c9d-9127-e69407fe748d"
+ XCTAssertEqual(expected, SUT.tifa("e0ef86a8-6EBf-4c9d-9127-e69407fe748d"))
+ XCTAssertEqual(expected, SUT.tifa(" e0ef86a8- 6ebf-4c9 d-9127-e69407fe748d "))
+ }
+
+ func test_afai() throws {
+ let expected = "f:6e853799-ef31-4a30-8706-9742be254d38"
+ XCTAssertEqual(expected, SUT.afai("6E853799-EF31-4a30-8706-9742be254d38"))
+ XCTAssertEqual(expected, SUT.afai(" 6 e853799- ef31-4a30-8706-9742be254d38 "))
+ }
+
+ func test_netid() throws {
+ let expected = "n:_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ"
+ XCTAssertEqual(expected, SUT.netid(" _YV2v2Uhx3vqe H47Rrhzgr-4c3VNs xis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ "))
+ XCTAssertEqual(expected, SUT.netid("_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ"))
+ }
+
+ func test_custom() throws {
+ let expected = "c:FooBarBAZ-01234#98765.!!!"
+ XCTAssertEqual(expected, SUT.custom("FooBarBAZ-01234#98765.!!!"))
+ XCTAssertEqual(expected, SUT.custom(" FooBarBAZ-01234#98765.!!!"))
+ XCTAssertEqual(expected, SUT.custom("FooBarBAZ-01234#98765.!!! "))
+ XCTAssertEqual(expected, SUT.custom(" FooBarBAZ-01234#98765.!!! "))
+
+ // Case sensitive
+ let unexpected = "c:FooBarBAZ-01234#98765.!!!"
+ XCTAssertNotEqual(unexpected, SUT.custom("foobarBAZ-01234#98765.!!!"))
+ }
+
+ // MARK: Legacy
+ func test_eidFromURL_isCorrect() throws {
+ let url = "http://some.domain.com/some/path?some=query&something=else&oeid=a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3&foo=bar&baz"
+ let expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"
+
+ XCTAssertEqual(expected, SUT.eidFromURL(url))
+ }
+
+ func test_eidFromURL_returnsEmptyWhenArgEmpty() throws {
+ let url = ""
+ let expected = ""
+
+ XCTAssertEqual(expected, SUT.eidFromURL(url))
+ }
+
+ func test_eidFromURL_returnsEmptyWhenOeidAbsentFromQuerystring() throws {
+ let url = "http://some.domain.com/some/path?some=query&something=else"
+ let expected = ""
+
+ XCTAssertEqual(expected, SUT.eidFromURL(url))
+ }
+
+ func test_eidFromURL_returnsEmptyWhenQuerystringAbsent() throws {
+ let url = "http://some.domain.com/some/path"
+ let expected = ""
+
+ XCTAssertEqual(expected, SUT.eidFromURL(url))
+ }
+
+ func test_eidFromURL_expectsSHA256() throws {
+ let url = "http://some.domain.com/some/path?some=query&something=else&oeid=AAAAAAAa665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3&foo=bar&baz"
+ let expected = ""
+
+ XCTAssertEqual(expected, SUT.eidFromURL(url))
+ }
+
+ func test_eidFromURL_ignoresCase() throws {
+ let url = "http://some.domain.com/some/path?some=query&something=else&oEId=A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86f7f7A27AE3&foo=bar&baz"
+ let expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"
+
+ XCTAssertEqual(expected, SUT.eidFromURL(url))
+ }
+}
diff --git a/Tests/Unit/OptableIdentifiersTests.swift b/Tests/Unit/OptableIdentifiersTests.swift
new file mode 100644
index 0000000..0452e89
--- /dev/null
+++ b/Tests/Unit/OptableIdentifiersTests.swift
@@ -0,0 +1,132 @@
+//
+// OptableIdentifiersTests.swift
+// OptableSDK
+//
+// Created by user on 17.12.2025.
+// Copyright © 2025 Optable Technologies, Inc. All rights reserved.
+//
+
+@testable import OptableSDK
+import XCTest
+
+class OptableIdentifiersTests: XCTestCase {
+ func test_json_generation_empty() throws {
+ let expected = "[]"
+ let oids = OptableIdentifiers()
+ let data = try JSONEncoder().encode(oids)
+ let generatedJSON = String(data: data, encoding: .utf8)
+ XCTAssertEqual(expected, generatedJSON)
+ }
+
+ func test_json_generation_list_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:m5v3l9" }))
+ XCTAssertTrue(decodedData.contains(where: { $0 == "id5:ID5*UDWnp3JOtWV0ky-bHvEeU4xOVHXCmYeg24YigF8iAymUHplfYSElM3fy79h8p-Fg" }))
+ XCTAssertTrue(decodedData.contains(where: { $0 == "utiq:496f5db5-681f-4392-acd5-0d4f6e2f6b88" }))
+ XCTAssertTrue(decodedData.contains(where: { $0 == "i4:8.8.8.8" }))
+ XCTAssertTrue(decodedData.contains(where: { $0 == "i6:2001:0db8:85a3:0000:0000:8a2e:0370:7334" }))
+ XCTAssertTrue(decodedData.contains(where: { $0 == "a:496f5db5-681f-4392-acd5-0d4f6e2f6b88" }))
+ XCTAssertTrue(decodedData.contains(where: { $0 == "g:64873d9f-d5af-4770-8bcb-167a220eb17d" }))
+ XCTAssertTrue(decodedData.contains(where: { $0 == "r:0b179df0-6cd5-49f1-be21-425d002e0d22" }))
+ XCTAssertTrue(decodedData.contains(where: { $0 == "s:e0ef86a8-6ebf-4c9d-9127-e69407fe748d" }))
+ XCTAssertTrue(decodedData.contains(where: { $0 == "f:6e853799-ef31-4a30-8706-9742be254d38" }))
+ XCTAssertTrue(decodedData.contains(where: { $0 == "n:_YV2v2Uhx3vqeH47Rrhzgr-4c3VNsxis4M1WY9qn--QTbVapax5VM2HJykoGAyWcwS5lKQ" }))
+ XCTAssertTrue(decodedData.contains(where: { $0 == "c:d29c551097b9dd0b82423827f65161232efaf7fc" }))
+ XCTAssertTrue(decodedData.contains(where: { $0 == "c1:AaaZza.dh012" }))
+ // 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..7831b0a 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
+ Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2
+ GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c
OptableSDK: fc5d3852c29fac1881b1d3ab6ea397de71c8cbf1
+ 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..a174292 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,13 @@
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 */; };
+ 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 +33,20 @@
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 = ""; };
+ 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 +61,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 +78,6 @@
isa = PBXGroup;
children = (
6320EEFB2535F92300F76877 /* demo-ios-objc */,
- 6320EF172535F92600F76877 /* demo-ios-objcTests */,
- 6320EF222535F92600F76877 /* demo-ios-objcUITests */,
6320EEFA2535F92300F76877 /* Products */,
689016CDEAF48669106E413E /* Pods */,
2CA740F147AE15FDA936899B /* Frameworks */,
@@ -130,8 +88,6 @@
isa = PBXGroup;
children = (
6320EEF92535F92300F76877 /* demo-ios-objc.app */,
- 6320EF142535F92600F76877 /* demo-ios-objcTests.xctest */,
- 6320EF1F2535F92600F76877 /* demo-ios-objcUITests.xctest */,
);
name = Products;
sourceTree = "";
@@ -149,6 +105,8 @@
6320EF032535F92300F76877 /* IdentifyViewController.m */,
63B5A8C52536704F000CA436 /* GAMBannerViewController.h */,
63B5A8C025366FE8000CA436 /* GAMBannerViewController.m */,
+ CE9FB6142EF9B9D100277231 /* PrebidBannerViewController.h */,
+ CE9FB6152EF9B9D100277231 /* PrebidBannerViewController.m */,
6320EF052535F92300F76877 /* Main.storyboard */,
6320EF082535F92500F76877 /* Assets.xcassets */,
6320EF0A2535F92500F76877 /* LaunchScreen.storyboard */,
@@ -158,24 +116,6 @@
path = "demo-ios-objc";
sourceTree = "";
};
- 6320EF172535F92600F76877 /* demo-ios-objcTests */ = {
- isa = PBXGroup;
- children = (
- 6320EF182535F92600F76877 /* demo_ios_objcTests.m */,
- 6320EF1A2535F92600F76877 /* Info.plist */,
- );
- path = "demo-ios-objcTests";
- sourceTree = "";
- };
- 6320EF222535F92600F76877 /* demo-ios-objcUITests */ = {
- isa = PBXGroup;
- children = (
- 6320EF232535F92600F76877 /* demo_ios_objcUITests.m */,
- 6320EF252535F92600F76877 /* Info.plist */,
- );
- path = "demo-ios-objcUITests";
- sourceTree = "";
- };
689016CDEAF48669106E413E /* Pods */ = {
isa = PBXGroup;
children = (
@@ -212,46 +152,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 +164,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 +180,6 @@
projectRoot = "";
targets = (
6320EEF82535F92300F76877 /* demo-ios-objc */,
- 6320EF132535F92600F76877 /* demo-ios-objcTests */,
- 6320EF1E2535F92600F76877 /* demo-ios-objcUITests */,
);
};
/* End PBXProject section */
@@ -305,20 +195,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 +241,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 +262,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 +269,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ CE9FB6162EF9B9D100277231 /* PrebidBannerViewController.m in Sources */,
63B5A91E2536825A000CA436 /* IdentifyViewController.h in Sources */,
6320EF042535F92300F76877 /* IdentifyViewController.m in Sources */,
63B5A91A25368252000CA436 /* GAMBannerViewController.h in Sources */,
@@ -490,37 +281,8 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
- 6320EF102535F92600F76877 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 6320EF192535F92600F76877 /* demo_ios_objcTests.m in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 6320EF1B2535F92600F76877 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 6320EF242535F92600F76877 /* demo_ios_objcUITests.m in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
/* End PBXSourcesBuildPhase section */
-/* Begin PBXTargetDependency section */
- 6320EF162535F92600F76877 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = 6320EEF82535F92300F76877 /* demo-ios-objc */;
- targetProxy = 6320EF152535F92600F76877 /* PBXContainerItemProxy */;
- };
- 6320EF212535F92600F76877 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = 6320EEF82535F92300F76877 /* demo-ios-objc */;
- targetProxy = 6320EF202535F92600F76877 /* PBXContainerItemProxy */;
- };
-/* End PBXTargetDependency section */
-
/* Begin PBXVariantGroup section */
6320EF052535F92300F76877 /* Main.storyboard */ = {
isa = PBXVariantGroup;
@@ -700,88 +462,6 @@
};
name = Release;
};
- 6320EF2C2535F92600F76877 /* Debug */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 83DB069E12658B3EA13B8867 /* Pods-demo-ios-objcTests.debug.xcconfig */;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- DEVELOPMENT_TEAM = "";
- INFOPLIST_FILE = "demo-ios-objcTests/Info.plist";
- IPHONEOS_DEPLOYMENT_TARGET = 14.0;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- "@loader_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-objcTests";
- PRODUCT_NAME = "$(TARGET_NAME)";
- TARGETED_DEVICE_FAMILY = "1,2";
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/demo-ios-objc.app/demo-ios-objc";
- };
- name = Debug;
- };
- 6320EF2D2535F92600F76877 /* Release */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 227B8E60A0CD66C45A87270D /* Pods-demo-ios-objcTests.release.xcconfig */;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- DEVELOPMENT_TEAM = "";
- INFOPLIST_FILE = "demo-ios-objcTests/Info.plist";
- IPHONEOS_DEPLOYMENT_TARGET = 14.0;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- "@loader_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-objcTests";
- PRODUCT_NAME = "$(TARGET_NAME)";
- TARGETED_DEVICE_FAMILY = "1,2";
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/demo-ios-objc.app/demo-ios-objc";
- };
- name = Release;
- };
- 6320EF2F2535F92600F76877 /* Debug */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 45728C7558D469A46F00466E /* Pods-demo-ios-objc-demo-ios-objcUITests.debug.xcconfig */;
- buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
- CODE_SIGN_STYLE = Automatic;
- DEVELOPMENT_TEAM = "";
- INFOPLIST_FILE = "demo-ios-objcUITests/Info.plist";
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- "@loader_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-objcUITests";
- PRODUCT_NAME = "$(TARGET_NAME)";
- TARGETED_DEVICE_FAMILY = "1,2";
- TEST_TARGET_NAME = "demo-ios-objc";
- };
- name = Debug;
- };
- 6320EF302535F92600F76877 /* Release */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 523A0EFF3A972FC18833C15D /* Pods-demo-ios-objc-demo-ios-objcUITests.release.xcconfig */;
- buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
- CODE_SIGN_STYLE = Automatic;
- DEVELOPMENT_TEAM = "";
- INFOPLIST_FILE = "demo-ios-objcUITests/Info.plist";
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- "@loader_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = "co.optable.demo-ios-objcUITests";
- PRODUCT_NAME = "$(TARGET_NAME)";
- TARGETED_DEVICE_FAMILY = "1,2";
- TEST_TARGET_NAME = "demo-ios-objc";
- };
- name = Release;
- };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -803,24 +483,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..98e711c 100644
--- a/demo-ios-objc/demo-ios-objc/AppDelegate.m
+++ b/demo-ios-objc/demo-ios-objc/AppDelegate.m
@@ -8,8 +8,12 @@
#import "AppDelegate.h"
#import "OptableSDKDelegate.h"
+
@import OptableSDK;
+@import PrebidMobile;
+@import GoogleMobileAds;
+
OptableSDK *OPTABLE = nil;
@interface AppDelegate ()
@@ -18,15 +22,33 @@ @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];
+ 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..c3e01bf 100644
--- a/demo-ios-objc/demo-ios-objc/Base.lproj/Main.storyboard
+++ b/demo-ios-objc/demo-ios-objc/Base.lproj/Main.storyboard
@@ -204,6 +204,7 @@
+
@@ -248,6 +249,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -259,7 +368,7 @@
-
+
diff --git a/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m b/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m
index fd27a96..dd6203c 100644
--- a/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m
+++ b/demo-ios-objc/demo-ios-objc/GAMBannerViewController.m
@@ -20,16 +20,20 @@ @interface GAMBannerViewController ()
@implementation GAMBannerViewController
+- (NSString *)AD_MANAGER_AD_UNIT_ID {
+ return @"/22081946781/ios-sdk-demo/mobile-leaderboard";
+}
+
- (void)viewDidLoad {
[super viewDidLoad];
self.bannerView = [[GADBannerView alloc] initWithAdSize:GADAdSizeBanner];
- self.bannerView.adUnitID = @"/22081946781/ios-sdk-demo/mobile-leaderboard";
- [self addBannerViewToView:self.bannerView];
+ self.bannerView.adUnitID = self.AD_MANAGER_AD_UNIT_ID;
self.bannerView.rootViewController = self;
+ [self addBannerViewToView:self.bannerView];
OptableSDKDelegate *delegate = (OptableSDKDelegate *)OPTABLE.delegate;
- delegate.bannerView = self.bannerView;
+ delegate.adManagerBannerView = self.bannerView;
delegate.targetingOutput = self.targetingOutput;
}
@@ -38,9 +42,14 @@ - (IBAction)loadBannerWithTargeting:(id)sender {
[_targetingOutput setText:@"Calling /targeting API...\n"];
- [OPTABLE targetingAndReturnError:&error];
- [OPTABLE witness:@"GAMBannerViewController.loadBannerClicked" properties:@{ @"example": @"value" } error:&error];
- [OPTABLE profileWithTraits:@{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES } error:&error];
+ [OPTABLE targetingWithIds: NULL error: &error];
+ [OPTABLE witnessWithEvent: @"GAMBannerViewController.loadBannerClicked"
+ properties: @{ @"example": @"value" }
+ error: &error];
+ [OPTABLE profileWithTraits: @{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES }
+ id: NULL
+ neighbors: NULL
+ error: &error];
}
- (IBAction)loadBannerWithTargetingFromCache:(id)sender {
@@ -48,22 +57,27 @@ - (IBAction)loadBannerWithTargetingFromCache:(id)sender {
GAMRequest *request = [GAMRequest request];
NSDictionary *keyvals = nil;
- [_targetingOutput setText:@"Checking local targeting cache...\n\n"];
+ [_targetingOutput setText:@"🗂 Checking local targeting cache...\n\n"];
keyvals = [OPTABLE targetingFromCache];
if (keyvals != nil) {
request.customTargeting = keyvals;
NSLog(@"[OptableSDK] Cached targeting values found: %@", keyvals);
- [_targetingOutput setText:[NSString stringWithFormat:@"%@\nFound cached data: %@\n", [_targetingOutput text], keyvals]];
+ [_targetingOutput setText:[NSString stringWithFormat:@"%@\n✅ Found cached data: %@\n", [_targetingOutput text], keyvals]];
} else {
- [_targetingOutput setText:[NSString stringWithFormat:@"%@\nCache empty.\n",
+ [_targetingOutput setText:[NSString stringWithFormat:@"%@\nℹ️ Cache empty.\n",
[_targetingOutput text]]];
}
[self.bannerView loadRequest:request];
- [OPTABLE witness:@"GAMBannerViewController.loadBannerClicked" properties:@{ @"example": @"value" } error:&error];
- [OPTABLE profileWithTraits:@{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES } error:&error];
+ [OPTABLE witnessWithEvent: @"GAMBannerViewController.loadBannerClicked"
+ properties: @{ @"example": @"value" }
+ error: &error];
+ [OPTABLE profileWithTraits: @{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES }
+ id: NULL
+ neighbors: NULL
+ error: &error];
}
- (IBAction)clearTargetingCache:(id)sender {
@@ -71,6 +85,8 @@ - (IBAction)clearTargetingCache:(id)sender {
[OPTABLE targetingClearCache];
}
+// MARK: - Helpers
+
- (void)addBannerViewToView:(UIView *)bannerView {
bannerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.adPlaceholder addSubview:bannerView];
diff --git a/demo-ios-objc/demo-ios-objc/IdentifyViewController.m b/demo-ios-objc/demo-ios-objc/IdentifyViewController.m
index e046adb..187eac9 100644
--- a/demo-ios-objc/demo-ios-objc/IdentifyViewController.m
+++ b/demo-ios-objc/demo-ios-objc/IdentifyViewController.m
@@ -18,25 +18,25 @@ @implementation IdentifyViewController
- (void)viewDidLoad {
[super viewDidLoad];
-
+
OptableSDKDelegate *delegate = (OptableSDKDelegate *)OPTABLE.delegate;
delegate.identifyOutput = self.identifyOutput;
}
- (IBAction)dispatchIdentify:(id)sender {
- NSString *email = [_identifyInput text];
- bool aaid = [_identifyIDFA isOn];
- NSMutableString *output;
- NSError *error = nil;
-
- output = [NSMutableString stringWithFormat:@"Calling /identify API with:\n\n"];
- if ([email length] > 0) {
- [output appendString:[NSString stringWithFormat:@"Email: %@\n", email]];
+ NSString *email = _identifyInput.text;
+ BOOL aaid = _identifyIDFA.isOn;
+
+ NSMutableString *output = [NSMutableString stringWithFormat: @"Calling /identify API with:\n\n"];
+ if (email.length > 0) {
+ [output appendString: [NSString stringWithFormat: @"Email: %@\n", email]];
}
- [output appendString:[NSString stringWithFormat:@"IDFA: %s\n", aaid ? "true" : "false"]];
- [_identifyOutput setText:output];
-
- [OPTABLE identify :email aaid:aaid ppid:@"" error:&error];
+ [output appendString: [NSString stringWithFormat: @"IDFA: %s\n", aaid ? "true" : "false"]];
+ _identifyOutput.text = output;
+
+ NSError *error = nil;
+ NSDictionary *ids = @{ @"e" : email, @"c" : @"new-custom.ABC" };
+ [OPTABLE identify: ids error: &error];
}
@end
diff --git a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.h b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.h
index ae1a2f1..a97ac54 100644
--- a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.h
+++ b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.h
@@ -7,11 +7,19 @@
//
@import OptableSDK;
+
+@import PrebidMobile;
@import GoogleMobileAds;
@interface OptableSDKDelegate: NSObject
-@property(atomic, readwrite, strong) GADBannerView *bannerView;
+// MARK: - PrebidMobile
+@property(atomic, readwrite, weak) BannerAdUnit *pbmBannerAdUnit;
+
+// MARK: - GoogleMobileAds
+@property(atomic, readwrite, weak) GADBannerView *adManagerBannerView;
+
+// MARK: - Text Output
@property(atomic, readwrite, strong) UITextView *identifyOutput;
@property(atomic, readwrite, strong) UITextView *targetingOutput;
diff --git a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m
index 67e0774..eee1c4a 100644
--- a/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m
+++ b/demo-ios-objc/demo-ios-objc/OptableSDKDelegate.m
@@ -16,63 +16,100 @@ @interface OptableSDKDelegate ()
@implementation OptableSDKDelegate
- (void)identifyOk:(NSHTTPURLResponse *)result {
NSLog(@"[OptableSDK] Success on /identify API call. HTTP Status Code: %ld", result.statusCode);
+
dispatch_async(dispatch_get_main_queue(), ^{
- [self.identifyOutput setText:[NSString stringWithFormat:@"%@\n✅ Success", [self.identifyOutput text]]];
+ NSString *output = [NSString stringWithFormat: @"%@\n✅ Success", self.identifyOutput.text];
+ self.identifyOutput.text = output;
});
}
- (void)identifyErr:(NSError *)error {
NSLog(@"[OptableSDK] Error on /identify API call: %@", [error localizedDescription]);
+
dispatch_async(dispatch_get_main_queue(), ^{
- [self.identifyOutput setText:[NSString stringWithFormat:@"%@\n🚫 Error: %@\n", [self.identifyOutput text], [error localizedDescription]]];
+ NSString *output = [NSString stringWithFormat: @"%@\n🚫 Error: %@\n", self.identifyOutput.text, error.localizedDescription];
+ self.identifyOutput.text = output;
});
}
- (void)profileOk:(NSHTTPURLResponse *)result {
NSLog(@"[OptableSDK] Success on /profile API call. HTTP Status Code: %ld", result.statusCode);
dispatch_async(dispatch_get_main_queue(), ^{
- [self.targetingOutput setText:[NSString stringWithFormat:@"%@\n✅ Success calling profile API to set example traits.\n", [self.targetingOutput text]]];
+ NSString* output = [NSString stringWithFormat: @"%@\n✅ Success calling profile API to set example traits.\n", self.targetingOutput.text];
+ self.targetingOutput.text = output;
});
}
- (void)profileErr:(NSError *)error {
NSLog(@"[OptableSDK] Error on /profile API call: %@", [error localizedDescription]);
dispatch_async(dispatch_get_main_queue(), ^{
- [self.targetingOutput setText:[NSString stringWithFormat:@"%@\n🚫 Error: %@\n", [self.targetingOutput text], [error localizedDescription]]];
+ NSString* output = [NSString stringWithFormat: @"%@\n🚫 Error: %@\n", self.targetingOutput.text, error.localizedDescription];
+ self.targetingOutput.text = output;
});
}
- (void)targetingOk:(NSDictionary *)result {
// Update the GAM banner view with result targeting keyvalues:
GAMRequest *request = [GAMRequest request];
request.customTargeting = result;
- [self.bannerView loadRequest:request];
-
+ [self loadBannerWithKeyValues: result];
+
NSLog(@"[OptableSDK] Success on /targeting API call: %@", result);
+
dispatch_async(dispatch_get_main_queue(), ^{
- [self.targetingOutput setText:[NSString stringWithFormat:@"%@\nData: %@\n", [self.targetingOutput text], result]];
+ NSString* output = [NSString stringWithFormat: @"%@\nData: %@\n", self.targetingOutput.text, result];
+ self.targetingOutput.text = output;
});
}
- (void)targetingErr:(NSError *)error {
// Update the GAM banner view without targeting data:
GAMRequest *request = [GAMRequest request];
- [self.bannerView loadRequest:request];
-
+ [self.adManagerBannerView loadRequest:request];
+
NSLog(@"[OptableSDK] Error on /targeting API call: %@", [error localizedDescription]);
+
dispatch_async(dispatch_get_main_queue(), ^{
- [self.targetingOutput setText:[NSString stringWithFormat:@"%@\n🚫 Error: %@\n", [self.targetingOutput text], [error localizedDescription]]];
+ NSString* output = [NSString stringWithFormat: @"%@\n🚫 Error: %@\n", self.targetingOutput.text, error.localizedDescription];
+ self.targetingOutput.text = output;
});
}
- (void)witnessOk:(NSHTTPURLResponse *)result {
NSLog(@"[OptableSDK] Success on /witness API call. HTTP Status Code: %ld", result.statusCode);
dispatch_async(dispatch_get_main_queue(), ^{
- [self.targetingOutput setText:[NSString stringWithFormat:@"%@\n✅ Success calling witness API to log loadBannerClicked event.\n", [self.targetingOutput text]]];
+ NSString* output = [NSString stringWithFormat: @"%@\n✅ Success calling witness API to log loadBannerClicked event.\n", self.targetingOutput.text];
+ self.targetingOutput.text = output;
});
}
- (void)witnessErr:(NSError *)error {
NSLog(@"[OptableSDK] Error on /witness API call: %@", [error localizedDescription]);
dispatch_async(dispatch_get_main_queue(), ^{
- [self.targetingOutput setText:[NSString stringWithFormat:@"%@\n🚫 Error: %@\n", [self.targetingOutput text], [error localizedDescription]]];
+ NSString* output = [NSString stringWithFormat:@"%@\n🚫 Error: %@\n", self.targetingOutput.text, error.localizedDescription];
+ self.targetingOutput.text = output;
});
}
+
+- (void)loadBannerWithKeyValues:(NSDictionary * _Nullable)keyValues {
+ GAMRequest *request = [GAMRequest request];
+
+ if (self.pbmBannerAdUnit) {
+ __weak typeof(self) weakSelf = self;
+ [self.pbmBannerAdUnit fetchDemandWithAdObject:request completion:^(enum ResultCode status) {
+ if (status != ResultCodePrebidDemandFetchSuccess) {
+ NSLog(@"[PrebidMobile SDK] Prebid fetch demand failed: %ld", (long)status);
+ }
+ if (keyValues.count > 0) {
+ NSMutableDictionary *merged = [request.customTargeting mutableCopy] ?: [NSMutableDictionary dictionary];
+ [merged addEntriesFromDictionary:keyValues];
+ request.customTargeting = merged;
+ }
+ [weakSelf.adManagerBannerView loadRequest:request];
+ }];
+ } else {
+ if (keyValues.count > 0) {
+ request.customTargeting = keyValues;
+ }
+ [self.adManagerBannerView loadRequest:request];
+ }
+}
+
@end
diff --git a/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.h b/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.h
new file mode 100644
index 0000000..b2ece55
--- /dev/null
+++ b/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.h
@@ -0,0 +1,22 @@
+//
+// PrebidBannerViewController.h
+// demo-ios-objc
+//
+// Copyright © 2020 Optable Technologies Inc. All rights reserved.
+// See LICENSE for details.
+//
+
+#import
+
+@interface PrebidBannerViewController : UIViewController
+
+@property (weak, nonatomic) IBOutlet UIView *adPlaceholder;
+
+@property (weak, nonatomic) IBOutlet UIButton *loadBannerButton;
+@property (weak, nonatomic) IBOutlet UIButton *cachedBannerButton;
+@property (weak, nonatomic) IBOutlet UIButton *clearTargetingCacheButton;
+@property (weak, nonatomic) IBOutlet UITextView *targetingOutput;
+
+- (IBAction)loadBannerWithTargeting:(id)sender;
+
+@end
diff --git a/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.m b/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.m
new file mode 100644
index 0000000..901adf5
--- /dev/null
+++ b/demo-ios-objc/demo-ios-objc/PrebidBannerViewController.m
@@ -0,0 +1,112 @@
+//
+// PrebidBannerViewController.m
+// demo-ios-objc
+//
+// Copyright © 2020 Optable Technologies Inc. All rights reserved.
+// See LICENSE for details.
+//
+
+#import "OptableSDKDelegate.h"
+#import "PrebidBannerViewController.h"
+#import "AppDelegate.h"
+
+@import OptableSDK;
+@import GoogleMobileAds;
+
+@interface PrebidBannerViewController ()
+
+@property(nonatomic, strong) GADBannerView *bannerView;
+@property(nonatomic, strong) BannerAdUnit *pbmBannerAdUnit;
+
+@end
+
+@implementation PrebidBannerViewController
+
+- (NSString *)AD_MANAGER_AD_UNIT_ID {
+ return @"/21808260008/prebid_demo_app_original_api_banner";
+}
+
+- (NSString *)PREBID_STORED_IMP {
+ return @"prebid-demo-banner-320-50";
+}
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ self.bannerView = [[GADBannerView alloc] initWithAdSize:GADAdSizeBanner];
+ self.bannerView.adUnitID = self.AD_MANAGER_AD_UNIT_ID;
+ self.bannerView.rootViewController = self;
+ [self addBannerViewToView:self.bannerView];
+
+ self.pbmBannerAdUnit = [[BannerAdUnit alloc] initWithConfigId:self.PREBID_STORED_IMP
+ size:CGSizeMake(320, 50)];
+
+ OptableSDKDelegate *delegate = (OptableSDKDelegate *)OPTABLE.delegate;
+ delegate.adManagerBannerView = self.bannerView;
+ delegate.pbmBannerAdUnit = self.pbmBannerAdUnit;
+ delegate.targetingOutput = self.targetingOutput;
+}
+
+- (IBAction)loadBannerWithTargeting:(id)sender {
+ NSError *error = nil;
+
+ [_targetingOutput setText:@"📡 Calling /targeting API...\n"];
+
+ [OPTABLE targetingWithIds: NULL error: &error];
+ [OPTABLE witnessWithEvent: @"PrebidBannerViewController.loadBannerClicked"
+ properties: @{ @"example": @"value" }
+ error: &error];
+ [OPTABLE profileWithTraits: @{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES }
+ id: NULL
+ neighbors: NULL
+ error: &error];
+}
+
+- (IBAction)loadBannerWithTargetingFromCache:(id)sender {
+ NSError *error = nil;
+ GAMRequest *request = [GAMRequest request];
+ NSDictionary *keyvals = nil;
+
+ [_targetingOutput setText:@"🗂 Checking local targeting cache...\n\n"];
+
+ keyvals = [OPTABLE targetingFromCache];
+
+ if (keyvals != nil) {
+ request.customTargeting = keyvals;
+ NSLog(@"[OptableSDK] Cached targeting values found: %@", keyvals);
+ [_targetingOutput setText:[NSString stringWithFormat:@"%@\n✅ Found cached data: %@\n", [_targetingOutput text], keyvals]];
+ } else {
+ [_targetingOutput setText:[NSString stringWithFormat:@"%@\nℹ️ Cache empty.\n",
+ [_targetingOutput text]]];
+ }
+
+ [self.bannerView loadRequest:request];
+
+
+ [OPTABLE witnessWithEvent: @"PrebidBannerViewController.loadBannerClicked"
+ properties: @{ @"example": @"value" }
+ error: &error];
+ [OPTABLE profileWithTraits: @{ @"example": @"value", @"anotherExample": @123, @"thirdExample": @YES }
+ id: NULL
+ neighbors: NULL
+ error: &error];
+}
+
+- (IBAction)clearTargetingCache:(id)sender {
+ [_targetingOutput setText:@"🧹 Clearing local targeting cache.\n"];
+ [OPTABLE targetingClearCache];
+}
+
+// MARK: - Helpers
+
+- (void)addBannerViewToView:(UIView *)bannerView {
+ bannerView.translatesAutoresizingMaskIntoConstraints = NO;
+ [self.adPlaceholder addSubview:bannerView];
+
+ [NSLayoutConstraint activateConstraints:@[
+ [bannerView.centerXAnchor constraintEqualToAnchor:self.adPlaceholder.centerXAnchor],
+ [bannerView.centerYAnchor constraintEqualToAnchor:self.adPlaceholder.centerYAnchor]
+ ]];
+}
+
+@end
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..0624621 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
+ Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2
+ GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c
OptableSDK: fc5d3852c29fac1881b1d3ab6ea397de71c8cbf1
+ 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..6063a80 100644
--- a/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj
+++ b/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj
@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
3E8C4D28A821E9F0A202EA9D /* Pods_demo_ios_swift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D76144A328FE0069ABDF4B5F /* Pods_demo_ios_swift.framework */; };
+ 536D9E922EA0DD37006D86BE /* PrebidBannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D9E912EA0DD37006D86BE /* PrebidBannerViewController.swift */; };
631466ED24F7F555007DCA5D /* GAMBannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631466EC24F7F555007DCA5D /* GAMBannerViewController.swift */; };
6352AA5B24DC7AE9002E66EB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6352AA5A24DC7AE9002E66EB /* AppDelegate.swift */; };
6352AA5D24DC7AE9002E66EB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6352AA5C24DC7AE9002E66EB /* SceneDelegate.swift */; };
@@ -15,29 +16,8 @@
6352AA6224DC7AE9002E66EB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6352AA6024DC7AE9002E66EB /* Main.storyboard */; };
6352AA6424DC7AEC002E66EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6352AA6324DC7AEC002E66EB /* Assets.xcassets */; };
6352AA6724DC7AEC002E66EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6352AA6524DC7AEC002E66EB /* LaunchScreen.storyboard */; };
- 6352AA7224DC7AEC002E66EB /* demo_ios_swiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6352AA7124DC7AEC002E66EB /* demo_ios_swiftTests.swift */; };
- 6352AA7D24DC7AEC002E66EB /* demo_ios_swiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6352AA7C24DC7AEC002E66EB /* demo_ios_swiftUITests.swift */; };
- 8A63354DEF86B0AD26566B2E /* Pods_demo_ios_swiftTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A022CEF25F2CA644724048D /* Pods_demo_ios_swiftTests.framework */; };
- DD7B19462B7FBA17A7ED90D3 /* Pods_demo_ios_swift_demo_ios_swiftUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D56163CC808207FC0184D7A /* Pods_demo_ios_swift_demo_ios_swiftUITests.framework */; };
/* End PBXBuildFile section */
-/* Begin PBXContainerItemProxy section */
- 6352AA6E24DC7AEC002E66EB /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6352AA4F24DC7AE9002E66EB /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = 6352AA5624DC7AE9002E66EB;
- remoteInfo = "demo-ios-swift";
- };
- 6352AA7924DC7AEC002E66EB /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6352AA4F24DC7AE9002E66EB /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = 6352AA5624DC7AE9002E66EB;
- remoteInfo = "demo-ios-swift";
- };
-/* End PBXContainerItemProxy section */
-
/* Begin PBXCopyFilesBuildPhase section */
63C1E32924EAD80B00C4FE51 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
@@ -54,6 +34,7 @@
/* Begin PBXFileReference section */
0A022CEF25F2CA644724048D /* Pods_demo_ios_swiftTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_demo_ios_swiftTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3386157F1FDF211F3621C6CF /* Pods-demo-ios-swift-demo-ios-swiftUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-swift-demo-ios-swiftUITests.debug.xcconfig"; path = "Target Support Files/Pods-demo-ios-swift-demo-ios-swiftUITests/Pods-demo-ios-swift-demo-ios-swiftUITests.debug.xcconfig"; sourceTree = ""; };
+ 536D9E912EA0DD37006D86BE /* PrebidBannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidBannerViewController.swift; sourceTree = ""; };
631466EC24F7F555007DCA5D /* GAMBannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GAMBannerViewController.swift; sourceTree = ""; };
6352AA5724DC7AE9002E66EB /* demo-ios-swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "demo-ios-swift.app"; sourceTree = BUILT_PRODUCTS_DIR; };
6352AA5A24DC7AE9002E66EB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
@@ -63,12 +44,6 @@
6352AA6324DC7AEC002E66EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
6352AA6624DC7AEC002E66EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
6352AA6824DC7AEC002E66EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- 6352AA6D24DC7AEC002E66EB /* demo-ios-swiftTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "demo-ios-swiftTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
- 6352AA7124DC7AEC002E66EB /* demo_ios_swiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = demo_ios_swiftTests.swift; sourceTree = ""; };
- 6352AA7324DC7AEC002E66EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- 6352AA7824DC7AEC002E66EB /* demo-ios-swiftUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "demo-ios-swiftUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
- 6352AA7C24DC7AEC002E66EB /* demo_ios_swiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = demo_ios_swiftUITests.swift; sourceTree = ""; };
- 6352AA7E24DC7AEC002E66EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
7D56163CC808207FC0184D7A /* Pods_demo_ios_swift_demo_ios_swiftUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_demo_ios_swift_demo_ios_swiftUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A909BB3E86CA17B56519FA37 /* Pods-demo-ios-swift.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-swift.debug.xcconfig"; path = "Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift.debug.xcconfig"; sourceTree = ""; };
AC9D63E534725E310399A3FF /* Pods-demo-ios-swift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-demo-ios-swift.release.xcconfig"; path = "Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift.release.xcconfig"; sourceTree = ""; };
@@ -87,22 +62,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 +69,6 @@
isa = PBXGroup;
children = (
6352AA5924DC7AE9002E66EB /* demo-ios-swift */,
- 6352AA7024DC7AEC002E66EB /* demo-ios-swiftTests */,
- 6352AA7B24DC7AEC002E66EB /* demo-ios-swiftUITests */,
6352AA5824DC7AE9002E66EB /* Products */,
63C1E32324EAD73800C4FE51 /* Frameworks */,
8018ABD26BD652CFB14910C3 /* Pods */,
@@ -122,8 +79,6 @@
isa = PBXGroup;
children = (
6352AA5724DC7AE9002E66EB /* demo-ios-swift.app */,
- 6352AA6D24DC7AEC002E66EB /* demo-ios-swiftTests.xctest */,
- 6352AA7824DC7AEC002E66EB /* demo-ios-swiftUITests.xctest */,
);
name = Products;
sourceTree = "";
@@ -134,6 +89,7 @@
6352AA5A24DC7AE9002E66EB /* AppDelegate.swift */,
6352AA5C24DC7AE9002E66EB /* SceneDelegate.swift */,
631466EC24F7F555007DCA5D /* GAMBannerViewController.swift */,
+ 536D9E912EA0DD37006D86BE /* PrebidBannerViewController.swift */,
6352AA5E24DC7AE9002E66EB /* IdentifyViewController.swift */,
6352AA6024DC7AE9002E66EB /* Main.storyboard */,
6352AA6324DC7AEC002E66EB /* Assets.xcassets */,
@@ -143,24 +99,6 @@
path = "demo-ios-swift";
sourceTree = "";
};
- 6352AA7024DC7AEC002E66EB /* demo-ios-swiftTests */ = {
- isa = PBXGroup;
- children = (
- 6352AA7124DC7AEC002E66EB /* demo_ios_swiftTests.swift */,
- 6352AA7324DC7AEC002E66EB /* Info.plist */,
- );
- path = "demo-ios-swiftTests";
- sourceTree = "";
- };
- 6352AA7B24DC7AEC002E66EB /* demo-ios-swiftUITests */ = {
- isa = PBXGroup;
- children = (
- 6352AA7C24DC7AEC002E66EB /* demo_ios_swiftUITests.swift */,
- 6352AA7E24DC7AEC002E66EB /* Info.plist */,
- );
- path = "demo-ios-swiftUITests";
- sourceTree = "";
- };
63C1E32324EAD73800C4FE51 /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -208,46 +146,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 +159,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 +175,6 @@
projectRoot = "";
targets = (
6352AA5624DC7AE9002E66EB /* demo-ios-swift */,
- 6352AA6C24DC7AEC002E66EB /* demo-ios-swiftTests */,
- 6352AA7724DC7AEC002E66EB /* demo-ios-swiftUITests */,
);
};
/* End PBXProject section */
@@ -302,67 +190,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 +215,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 +223,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 +244,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift-resources-${CONFIGURATION}-input-files.xcfilelist",
);
+ inputPaths = (
+ );
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift-resources-${CONFIGURATION}-output-files.xcfilelist",
);
+ outputPaths = (
+ );
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demo-ios-swift/Pods-demo-ios-swift-resources.sh\"\n";
@@ -461,43 +265,15 @@
buildActionMask = 2147483647;
files = (
6352AA5F24DC7AE9002E66EB /* IdentifyViewController.swift in Sources */,
+ 536D9E922EA0DD37006D86BE /* PrebidBannerViewController.swift in Sources */,
6352AA5B24DC7AE9002E66EB /* AppDelegate.swift in Sources */,
631466ED24F7F555007DCA5D /* GAMBannerViewController.swift in Sources */,
6352AA5D24DC7AE9002E66EB /* SceneDelegate.swift in Sources */,
);
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 +446,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 +467,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..e0fd9ff 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,38 @@ 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.
+
+ let config = OptableConfig(
+ tenant: "prebidtest",
+ originSlug: "ios-sdk",
+ host: "prebidtest.cloud.optable.co",
+ skipAdvertisingIdDetection: false
+ )
+ OPTABLE = OptableSDK(config: config)
- // See comment further above on why we are initializing OptableSDK() from here:
- OPTABLE = OptableSDK(host: "sandbox.optable.co", app: "ios-sdk-demo")
+ initPrebidMobile()
+ initGoogleMobileAds()
return true
}
+ private func initPrebidMobile() {
+ Prebid.shared.prebidServerAccountId = "0689a263-318d-448b-a3d4-b02e8a709d9d"
+
+ try? Prebid.initializeSDK(
+ serverURL: "https://prebid-server-test-j.prebid.org/openrtb2/auction"
+ )
+ }
+
+ private func initGoogleMobileAds() {
+ MobileAds.shared.start()
+ }
+
// MARK: UISceneSession Lifecycle
func application(
diff --git a/demo-ios-swift/demo-ios-swift/Base.lproj/LaunchScreen.storyboard b/demo-ios-swift/demo-ios-swift/Base.lproj/LaunchScreen.storyboard
index 3ac9e6f..1e9f3a1 100644
--- a/demo-ios-swift/demo-ios-swift/Base.lproj/LaunchScreen.storyboard
+++ b/demo-ios-swift/demo-ios-swift/Base.lproj/LaunchScreen.storyboard
@@ -1,9 +1,11 @@
-
+
-
+
+
+
@@ -14,8 +16,8 @@
-
+
@@ -23,4 +25,9 @@
+
+
+
+
+
diff --git a/demo-ios-swift/demo-ios-swift/Base.lproj/Main.storyboard b/demo-ios-swift/demo-ios-swift/Base.lproj/Main.storyboard
index 1bf46bf..d4f9299 100644
--- a/demo-ios-swift/demo-ios-swift/Base.lproj/Main.storyboard
+++ b/demo-ios-swift/demo-ios-swift/Base.lproj/Main.storyboard
@@ -1,6 +1,6 @@
-
-
+
+
@@ -204,6 +204,7 @@
+
@@ -232,6 +233,7 @@
+
@@ -244,10 +246,117 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -259,7 +368,7 @@
-
+
diff --git a/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift b/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift
index 4a8fa0d..886bb35 100644
--- a/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift
+++ b/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift
@@ -9,12 +9,14 @@
import UIKit
import GoogleMobileAds
+fileprivate let AD_MANAGER_AD_UNIT_ID = "/22081946781/ios-sdk-demo/mobile-leaderboard"
+
class GAMBannerViewController: UIViewController {
+ // MARK: - GoogleMobileAds
var bannerView: BannerView!
- // MARK: Properties
-
+ // MARK: - Outlets
@IBOutlet weak var adPlaceholder: UIView!
@IBOutlet weak var loadBannerButton: UIButton!
@IBOutlet weak var loadBannerFromCacheButton: UIButton!
@@ -27,35 +29,28 @@ class GAMBannerViewController: UIViewController {
bannerView = BannerView(adSize: AdSizeBanner)
addBannerViewToView(bannerView)
bannerView.rootViewController = self
+ bannerView.adUnitID = AD_MANAGER_AD_UNIT_ID
}
- //MARK: Actions
-
@IBAction func loadBannerWithTargeting(_ sender: UIButton) {
+ setOutput("📡 Calling /targeting API...\n\n")
+
do {
- targetingOutput.text = "Calling /targeting API...\n\n"
-
- try OPTABLE!.targeting() { result in
+ try OPTABLE!.targeting() { [weak self] result in
var tdata: NSDictionary = [:]
switch result {
case .success(let keyvalues):
print("[OptableSDK] Success on /targeting API call: \(keyvalues)")
-
tdata = keyvalues
-
- DispatchQueue.main.async {
- self.targetingOutput.text += "Data: \(keyvalues)\n"
- }
+ self?.appendOutput("✅ Data: \(keyvalues)\n")
case .failure(let error):
print("[OptableSDK] Error on /targeting API call: \(error)")
- DispatchQueue.main.async {
- self.targetingOutput.text += "🚫 Error: \(error)\n"
- }
+ self?.appendOutput("🚫 Error: \(error)\n")
}
- self.loadBanner(adUnitID: "/22081946781/ios-sdk-demo/mobile-leaderboard", keyvalues: tdata)
+ self?.loadBanner(keyvalues: tdata)
}
} catch {
print("[OptableSDK] Exception: \(error)")
@@ -63,30 +58,28 @@ class GAMBannerViewController: UIViewController {
}
@IBAction func loadBannerWithTargetingFromCache(_ sender: UIButton) {
- var tdata: NSDictionary = [:]
-
- targetingOutput.text = "Checking local targeting cache...\n\n"
+ setOutput("🗂 Checking local targeting cache...\n\n")
+ var tdata: NSDictionary = [:]
let cachedValues = OPTABLE!.targetingFromCache()
- if (cachedValues != nil) {
- print("[OptableSDK] Cached targeting values found: \(cachedValues!)")
- targetingOutput.text += "\nFound cached data: \(cachedValues!)\n"
- tdata = cachedValues!
+
+ if let cachedValues {
+ print("[OptableSDK] Cached targeting values found: \(cachedValues)")
+ appendOutput("✅ Found cached data: \(cachedValues)\n")
+ tdata = cachedValues
} else {
- targetingOutput.text += "\nCache empty.\n"
+ appendOutput("ℹ️ Cache empty.\n")
}
- self.loadBanner(adUnitID: "/22081946781/ios-sdk-demo/mobile-leaderboard", keyvalues: tdata)
+ loadBanner(keyvalues: tdata)
}
@IBAction func clearTargetingCache(_ sender: UIButton) {
- targetingOutput.text = "🧹 Clearing local targeting cache.\n"
+ setOutput("🧹 Clearing local targeting cache.\n")
OPTABLE!.targetingClearCache()
}
-
- private func loadBanner(adUnitID: String, keyvalues: NSDictionary) {
- bannerView.adUnitID = adUnitID
-
+
+ private func loadBanner(keyvalues: NSDictionary) {
let req = AdManagerRequest()
req.customTargeting = keyvalues as? [String: String]
bannerView.load(req)
@@ -97,19 +90,15 @@ class GAMBannerViewController: UIViewController {
private func witness() {
do {
- try OPTABLE!.witness(event: "GAMBannerViewController.loadBannerClicked", properties: ["example": "value"]) { result in
+ try OPTABLE!.witness(
+ event: "GAMBannerViewController.loadBannerClicked",
+ properties: ["example": "value"]
+ ) { [weak self] result in
switch result {
- case .success(let response):
- print("[OptableSDK] Success on /witness API call: response.statusCode = \(response.statusCode)")
- DispatchQueue.main.async {
- self.targetingOutput.text += "\n✅ Success calling witness API to log loadBannerClicked event.\n"
- }
-
+ case .success:
+ self?.appendOutput("\n✅ Success calling witness API to log loadBannerClicked event.\n")
case .failure(let error):
- print("[OptableSDK] Error on /witness API call: \(error)")
- DispatchQueue.main.async {
- self.targetingOutput.text += "\n🚫 Error: \(error)"
- }
+ self?.appendOutput("\n🚫 Error: \(error)\n")
}
}
} catch {
@@ -119,19 +108,14 @@ class GAMBannerViewController: UIViewController {
private func profile() {
do {
- try OPTABLE!.profile(traits: ["example": "value", "anotherExample": 123, "thirdExample": true ]) { result in
+ try OPTABLE!.profile(
+ traits: ["example": "value", "anotherExample": 123, "thirdExample": true]
+ ) { [weak self] result in
switch result {
- case .success(let response):
- print("[OptableSDK] Success on /profile API call: response.statusCode = \(response.statusCode)")
- DispatchQueue.main.async {
- self.targetingOutput.text += "\n✅ Success calling profile API to set example traits.\n"
- }
-
+ case .success:
+ self?.appendOutput("\n✅ Success calling profile API to set example traits.\n")
case .failure(let error):
- print("[OptableSDK] Error on /profile API call: \(error)")
- DispatchQueue.main.async {
- self.targetingOutput.text += "\n🚫 Error: \(error)"
- }
+ self?.appendOutput("\n🚫 Error: \(error)\n")
}
}
} catch {
@@ -139,6 +123,20 @@ class GAMBannerViewController: UIViewController {
}
}
+ // MARK: - Helpers
+
+ private func setOutput(_ text: String) {
+ DispatchQueue.main.async {
+ self.targetingOutput.text = text
+ }
+ }
+
+ private func appendOutput(_ text: String) {
+ DispatchQueue.main.async {
+ self.targetingOutput.text += text
+ }
+ }
+
private func addBannerViewToView(_ bannerView: BannerView) {
bannerView.translatesAutoresizingMaskIntoConstraints = false
adPlaceholder.addSubview(bannerView)
diff --git a/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift b/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift
index cd11738..865d69f 100644
--- a/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift
+++ b/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift
@@ -6,18 +6,18 @@
// See LICENSE for details.
//
+import OptableSDK
import UIKit
class IdentifyViewController: UIViewController {
-
// MARK: Properties
- @IBOutlet weak var identifyInput: UITextField!
- @IBOutlet weak var identifyButton: UIButton!
- @IBOutlet weak var identifyIDFA: UISwitch!
- @IBOutlet weak var identifyOutput: UITextView!
+ @IBOutlet var identifyInput: UITextField!
+ @IBOutlet var identifyButton: UIButton!
+ @IBOutlet var identifyIDFA: UISwitch!
+ @IBOutlet var identifyOutput: UITextView!
// MARK: Actions
-
+
// dispatchIdentify() is the action invoked on a click on the "Identify" UIButton in our demo app. It initiates
// a call to the OptableSDK.identify() API and prints debugging information to the UI and debug console.
@IBAction func dispatchIdentify(_ sender: UIButton) {
@@ -26,26 +26,35 @@ class IdentifyViewController: UIViewController {
let aaid = identifyIDFA.isOn as Bool
identifyOutput.text = "Calling /identify API with:\n\n"
- if (email != "") {
+ if email != "" {
identifyOutput.text += "Email: " + email + "\n"
}
identifyOutput.text += "IDFA: " + String(aaid) + "\n"
- try OPTABLE!.identify(email: email, aaid: aaid) { result in
- switch result {
- case .success(let response):
- print("[OptableSDK] Success on /identify API call: response.statusCode = \(response.statusCode)")
- DispatchQueue.main.async {
- self.identifyOutput.text += "\n✅ Success."
- }
+ let idfa = "06DE8C6A-A431-4235-A262-E3A9C2CCEB34"
+ let gaid = "D04BB8C3-5A3E-4964-9757-D38365F59E6A"
+ let phoneNumber = "+1234567890"
+ let custom = "new-custom.ABC"
+ let custom9 = "custom-9-id"
- case .failure(let error):
- print("[OptableSDK] Error on /identify API call: \(error)")
- DispatchQueue.main.async {
- self.identifyOutput.text += "\n🚫 Error: \(error)"
+ try OPTABLE!.identify(
+ OptableIdentifiers(emailAddress: email, phoneNumber: phoneNumber, appleIDFA: idfa, googleGAID: gaid, custom: ["c": custom, "c9": custom9]),
+ { result in
+ switch result {
+ case let .success(response):
+ print("[OptableSDK] Success on /identify API call: response.statusCode = \(response.statusCode)")
+ DispatchQueue.main.async {
+ self.identifyOutput.text += "\n✅ Success. Response: \(response)"
+ }
+
+ case let .failure(error):
+ print("[OptableSDK] Error on /identify API call: \(error)")
+ DispatchQueue.main.async {
+ self.identifyOutput.text += "\n🚫 Error: \(error)"
+ }
}
}
- }
+ )
} catch {
print("[OptableSDK] Exception: \(error)")
diff --git a/demo-ios-swift/demo-ios-swift/PrebidBannerViewController.swift b/demo-ios-swift/demo-ios-swift/PrebidBannerViewController.swift
new file mode 100644
index 0000000..5d6b2d3
--- /dev/null
+++ b/demo-ios-swift/demo-ios-swift/PrebidBannerViewController.swift
@@ -0,0 +1,197 @@
+//
+// PrebidBannerViewController.swift
+// demo-ios-swift
+//
+// Copyright © 2020 Optable Technologies Inc. All rights reserved.
+// See LICENSE for details.
+//
+
+import UIKit
+import PrebidMobile
+import GoogleMobileAds
+
+fileprivate let AD_MANAGER_AD_UNIT_ID = "/21808260008/prebid_demo_app_original_api_banner"
+fileprivate let PREBID_STORED_IMP = "prebid-demo-banner-320-50"
+
+class PrebidBannerViewController: UIViewController {
+
+ // MARK: - PrebidMobile
+ private var pbmBannerAdUnit: BannerAdUnit!
+
+ // MARK: - GoogleMobileAds
+ private var adManagerBannerView: AdManagerBannerView!
+
+ // MARK: - Outlets
+ @IBOutlet weak var adPlaceholder: UIView!
+ @IBOutlet weak var loadBannerButton: UIButton!
+ @IBOutlet weak var loadBannerFromCacheButton: UIButton!
+ @IBOutlet weak var clearTargetingCacheButton: UIButton!
+ @IBOutlet weak var targetingOutput: UITextView!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ pbmBannerAdUnit = BannerAdUnit(
+ configId: PREBID_STORED_IMP,
+ size: .init(width: 320, height: 50)
+ )
+
+ adManagerBannerView = AdManagerBannerView(adSize: AdSizeBanner)
+ adManagerBannerView.adUnitID = AD_MANAGER_AD_UNIT_ID
+ adManagerBannerView.rootViewController = self
+ adManagerBannerView.delegate = self
+ addBannerViewToView(adManagerBannerView)
+ }
+
+ @IBAction func loadBannerWithTargeting(_ sender: UIButton) {
+ setOutput("📡 Calling /targeting API...\n\n")
+
+ do {
+ try OPTABLE!.targeting { [weak self] result in
+ var tdata: NSDictionary = [:]
+
+ switch result {
+ case .success(let keyvalues):
+ print("[OptableSDK] Success on /targeting API call: \(keyvalues)")
+ tdata = keyvalues
+ self?.appendOutput("✅ Targeting data:\n\(keyvalues)\n")
+
+ case .failure(let error):
+ print("[OptableSDK] Error on /targeting API call: \(error)")
+ self?.appendOutput("🚫 Error: \(error.localizedDescription)\n")
+ }
+
+ self?.loadBanner(keyvalues: tdata)
+ }
+ } catch {
+ print("[OptableSDK] Exception: \(error)")
+ appendOutput("⚠️ Exception: \(error.localizedDescription)\n")
+ }
+ }
+
+ @IBAction func loadBannerWithTargetingFromCache(_ sender: UIButton) {
+ setOutput("🗂 Checking local targeting cache...\n\n")
+
+ var tdata: NSDictionary = [:]
+ if let cachedValues = OPTABLE!.targetingFromCache() {
+ print("[OptableSDK] Cached targeting values found: \(cachedValues)")
+ appendOutput("✅ Found cached data:\n\(cachedValues)\n")
+ tdata = cachedValues
+ } else {
+ appendOutput("ℹ️ Cache empty.\n")
+ }
+
+ loadBanner(keyvalues: tdata)
+ }
+
+ @IBAction func clearTargetingCache(_ sender: UIButton) {
+ setOutput("🧹 Clearing local targeting cache.\n")
+ OPTABLE!.targetingClearCache()
+ }
+
+ private func loadBanner(keyvalues: NSDictionary) {
+ let request = AdManagerRequest()
+
+ pbmBannerAdUnit.fetchDemand(adObject: request) { [weak self] status in
+ if status != .prebidDemandFetchSuccess {
+ print("[PrebidMobile SDK] Prebid fetch demand was not successful: \(status.name())")
+ }
+
+ // TODO: - Where should keyvalues go in Prebid and GAM(?) ?
+ if let keyvalues = keyvalues as? [String: String] {
+ request.customTargeting?.merge(keyvalues, uniquingKeysWith: { $1 })
+ }
+
+ self?.adManagerBannerView.load(request)
+ }
+
+ witness()
+ profile()
+ }
+
+ private func witness() {
+ do {
+ try OPTABLE!.witness(
+ event: "PrebidBannerViewController.loadBannerClicked",
+ properties: ["example": "value"]
+ ) { [weak self] result in
+ switch result {
+ case .success(let response):
+ print("[OptableSDK] Witness success: \(response.statusCode)")
+ self?.appendOutput("✅ Witness API logged loadBannerClicked event.\n")
+ case .failure(let error):
+ print("[OptableSDK] Witness error: \(error)")
+ self?.appendOutput("🚫 Witness error: \(error.localizedDescription)\n")
+ }
+ }
+ } catch {
+ print("[OptableSDK] Exception: \(error)")
+ appendOutput("⚠️ Witness exception: \(error.localizedDescription)\n")
+ }
+ }
+
+ private func profile() {
+ do {
+ try OPTABLE!.profile(
+ traits: ["example": "value", "anotherExample": 123, "thirdExample": true]
+ ) { [weak self] result in
+ switch result {
+ case .success(let response):
+ print("[OptableSDK] Profile success: \(response.statusCode)")
+ self?.appendOutput("✅ Profile API set example traits.\n")
+ case .failure(let error):
+ print("[OptableSDK] Profile error: \(error)")
+ self?.appendOutput("🚫 Profile error: \(error.localizedDescription)\n")
+ }
+ }
+ } catch {
+ print("[OptableSDK] Exception: \(error)")
+ appendOutput("⚠️ Profile exception: \(error.localizedDescription)\n")
+ }
+ }
+
+ // MARK: - Helpers
+ private func addBannerViewToView(_ bannerView: AdManagerBannerView) {
+ bannerView.translatesAutoresizingMaskIntoConstraints = false
+ adPlaceholder.addSubview(bannerView)
+
+ NSLayoutConstraint.activate([
+ bannerView.centerXAnchor.constraint(equalTo: adPlaceholder.centerXAnchor),
+ bannerView.centerYAnchor.constraint(equalTo: adPlaceholder.centerYAnchor)
+ ])
+ }
+
+ /// Safely sets the full text of targetingOutput on the main thread.
+ private func setOutput(_ text: String) {
+ DispatchQueue.main.async {
+ self.targetingOutput.text = text
+ }
+ }
+
+ /// Appends text to targetingOutput on the main thread with line break.
+ private func appendOutput(_ text: String) {
+ DispatchQueue.main.async {
+ self.targetingOutput.text += "\n\(text)"
+ }
+ }
+}
+
+// MARK: - GoogleMobileAds.BannerViewDelegate
+extension PrebidBannerViewController: GoogleMobileAds.BannerViewDelegate {
+
+ func bannerViewDidReceiveAd(_ bannerView: GoogleMobileAds.BannerView) {
+ AdViewUtils.findPrebidCreativeSize(bannerView, success: { size in
+ guard let bannerView = bannerView as? AdManagerBannerView else { return }
+ bannerView.resize(adSizeFor(cgSize: size))
+ }, failure: { error in
+ print("[PrebidMobile SDK] Error finding creative size: \(error)")
+ })
+ }
+
+ func bannerView(
+ _ bannerView: GoogleMobileAds.BannerView,
+ didFailToReceiveAdWithError error: any Error
+ ) {
+ print("[GMA SDK] Failed to receive ad: \(error)")
+ }
+}
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)