From 09b1847a12d1b134ec267150986938f1a273013f Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Tue, 3 Nov 2026 14:29:33 -0700 Subject: [PATCH 1/2] Demo hashed IDs, identify from URL, profile id/neighbors and async/await The Swift demo only showed plaintext identify and completion-handler targeting. Add: - Identify screen: an "Identify (hashed email)" button sending .hem, and a URL field plus "Identify from URL" button calling tryIdentifyFromURL with a sample newsletter link carrying an oeid. - Prebid screen: targeting via async/await; the GAM screen keeps the completion handler, so both styles are shown. - Both banner screens: profile() also passes an id and neighbors, built with .extendedIdentifier. Co-Authored-By: Claude Opus 5.5 --- .../demo-ios-swift/Base.lproj/Main.storyboard | 50 ++++++++++++++++++- .../GAMBannerViewController.swift | 6 ++- .../IdentifyViewController.swift | 49 ++++++++++++++++++ .../PrebidBannerViewController.swift | 33 ++++++------ 4 files changed, 117 insertions(+), 21 deletions(-) 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..16500b0 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,49 @@ 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) + + do { + let email = identifyInput.text ?? "" + 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): From 6d40cd7eb8b33f6881f68ead708e94c298e4da23 Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Mon, 9 Nov 2026 15:20:00 -0700 Subject: [PATCH 2/2] Don't send a hashed empty Email from the Swift demo Co-Authored-By: Claude Opus 5.5 --- .../demo-ios-swift/IdentifyViewController.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift b/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift index 16500b0..5d5962e 100644 --- a/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift +++ b/demo-ios-swift/demo-ios-swift/IdentifyViewController.swift @@ -84,8 +84,14 @@ class IdentifyViewController: UIViewController { @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 { - let email = identifyInput.text ?? "" try OPTABLE!.identify([.hem(sha256Email(email))]) { result in switch result { case .success: