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 bb13586..063528c 100644
--- a/demo-ios-swift/demo-ios-swift/Base.lproj/Main.storyboard
+++ b/demo-ios-swift/demo-ios-swift/Base.lproj/Main.storyboard
@@ -51,8 +51,44 @@
+
+
+
+
+
+
+
+
+
+
-
+
@@ -68,8 +104,17 @@
-
+
+
+
+
+
+
+
+
+
+
@@ -81,6 +126,7 @@
+
diff --git a/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift b/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift
index 64f545a..70cf217 100644
--- a/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift
+++ b/demo-ios-swift/demo-ios-swift/GAMBannerViewController.swift
@@ -124,8 +124,12 @@ private extension GAMBannerViewController {
func profile() {
do {
+ // `id` and `neighbors` are sent as given, so build them with `.extendedIdentifier`,
+ // which hashes Email addresses and phone numbers before they leave the device.
try OPTABLE!.profile(
- traits: ["example": "value", "anotherExample": 123, "thirdExample": true]
+ traits: ["example": "value", "anotherExample": 123, "thirdExample": true],
+ id: OptableIdentifier.custom(nil, "demo-user-1234").extendedIdentifier,
+ neighbors: [OptableIdentifier.emailAddress("some.email@address.com").extendedIdentifier]
) { result in
switch result {
case let .success(optableTargeting):
diff --git a/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift b/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift
index 4ded59a..5d5962e 100644
--- a/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift
+++ b/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift
@@ -9,6 +9,7 @@
import OptableSDK
import UIKit
import AppTrackingTransparency
+import CryptoKit
// MARK: - IdentifyViewController
class IdentifyViewController: UIViewController {
@@ -16,6 +17,7 @@ class IdentifyViewController: UIViewController {
@IBOutlet var identifyInput: UITextField!
@IBOutlet var identifyButton: UIButton!
@IBOutlet var identifyOutput: UITextView!
+ @IBOutlet var identifyURLInput: UITextField!
// Logging
private var networkLogObserver: (any NSObjectProtocol)?
@@ -23,6 +25,10 @@ class IdentifyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
identifyInput.delegate = self
+ identifyURLInput.delegate = self
+
+ // A link like the ones in a newsletter Email, carrying the SHA256 of the reader's Email address
+ identifyURLInput.text = "https://example.com/newsletter?oeid=\(sha256Email("some.email@address.com"))"
}
override func viewWillAppear(_ animated: Bool) {
@@ -72,6 +78,55 @@ class IdentifyViewController: UIViewController {
}
}
+
+ // dispatchIdentifyHashed() sends the Email as a SHA256 hashed in the app, as you would with a hash from your own backend.
+ // `.hem` sends it as given; passing a hash to `.emailAddress` would hash it a second time.
+ @IBAction func dispatchIdentifyHashed(_ sender: UIButton) {
+ view.endEditing(true)
+
+ // An empty Email would otherwise be sent as the SHA256 of an empty string
+ let email = (identifyInput.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !email.isEmpty else {
+ identifyOutput.text = "Enter an Email to send it hashed."
+ return
+ }
+
+ do {
+ try OPTABLE!.identify([.hem(sha256Email(email))]) { result in
+ switch result {
+ case .success:
+ print("[OptableSDK] ✅ Success on /identify API call with a hashed Email")
+ case let .failure(error):
+ print("[OptableSDK] 🚫 Error on /identify API call: \(error)")
+ }
+ }
+ } catch {
+ print("[OptableSDK] 🚫 Exception on /identify API call: \(error)")
+ identifyOutput.text += "🚫 EXCEPTION: \(error)"
+ }
+ }
+
+ // dispatchIdentifyFromURL() passes a link to tryIdentifyFromURL(), as you would from a universal link handler.
+ // If the link has an `oeid` query parameter holding a SHA256, the SDK calls /identify with it; otherwise it does nothing.
+ @IBAction func dispatchIdentifyFromURL(_ sender: UIButton) {
+ view.endEditing(true)
+
+ do {
+ try OPTABLE!.tryIdentifyFromURL(identifyURLInput.text ?? "")
+ } catch {
+ print("[OptableSDK] 🚫 Exception on tryIdentifyFromURL: \(error)")
+ identifyOutput.text += "🚫 EXCEPTION: \(error)"
+ }
+ }
+}
+
+// MARK: - Helpers
+private extension IdentifyViewController {
+ /// SHA256 of the Email, normalized the way the SDK does it for `.emailAddress`: whitespace removed, lowercased.
+ func sha256Email(_ email: String) -> String {
+ let normalized = email.components(separatedBy: .whitespacesAndNewlines).joined().lowercased()
+ return SHA256.hash(data: Data(normalized.utf8)).map { String(format: "%02x", $0) }.joined()
+ }
}
// MARK: - UITextFieldDelegate
diff --git a/demo-ios-swift/demo-ios-swift/PrebidBannerViewController.swift b/demo-ios-swift/demo-ios-swift/PrebidBannerViewController.swift
index 1e3ab48..39944c2 100644
--- a/demo-ios-swift/demo-ios-swift/PrebidBannerViewController.swift
+++ b/demo-ios-swift/demo-ios-swift/PrebidBannerViewController.swift
@@ -55,25 +55,18 @@ final class PrebidBannerViewController: UIViewController { // Outlets
}
// MARK: Actions
+ // This screen uses the async/await API; the GAM Banner screen shows the same flow with a completion handler.
@IBAction func loadBannerWithTargeting(_ sender: UIButton) {
- do {
- try OPTABLE!.targeting { [weak self] result in
- // The completion handler is not called on the main thread, and ads must be loaded on it.
- DispatchQueue.main.async {
- switch result {
- case let .success(optableTargeting):
- print("[OptableSDK] ✅ Success on /targeting API call: \(optableTargeting)")
- self?.loadBanner(optableTargeting)
-
- case let .failure(error):
- print("[OptableSDK] 🚫 Error on /targeting API call: \(error)")
- self?.loadBanner()
- }
- }
+ // The Task inherits the main actor from this action, so the banner is loaded on the main thread.
+ Task {
+ do {
+ let optableTargeting = try await OPTABLE!.targeting()
+ print("[OptableSDK] ✅ Success on /targeting API call: \(optableTargeting)")
+ loadBanner(optableTargeting)
+ } catch {
+ print("[OptableSDK] 🚫 Error on /targeting API call: \(error)")
+ loadBanner()
}
- } catch {
- print("[OptableSDK] 🚫 Exception on /targeting API call: \(error)")
- targetingLog = "🚫 EXCEPTION: \(error)"
}
}
@@ -153,8 +146,12 @@ private extension PrebidBannerViewController {
func profile() {
do {
+ // `id` and `neighbors` are sent as given, so build them with `.extendedIdentifier`,
+ // which hashes Email addresses and phone numbers before they leave the device.
try OPTABLE!.profile(
- traits: ["example": "value", "anotherExample": 123, "thirdExample": true]
+ traits: ["example": "value", "anotherExample": 123, "thirdExample": true],
+ id: OptableIdentifier.custom(nil, "demo-user-1234").extendedIdentifier,
+ neighbors: [OptableIdentifier.emailAddress("some.email@address.com").extendedIdentifier]
) { result in
switch result {
case let .success(optableTargeting):