Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Source/Core/EdgeAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ final class EdgeAPI {
return request
}

func profile(traits: NSDictionary, id: String? = nil, neighbors: [String]? = nil) throws -> URLRequest? {
func profile(traits: [String: Any], id: String? = nil, neighbors: [String]? = nil) throws -> URLRequest? {
guard let url = buildEdgeAPIURL(endpoint: "profile") else { return nil }

var payload: [String: Any] = ["traits": traits]
Expand Down Expand Up @@ -75,7 +75,7 @@ final class EdgeAPI {
return request
}

func witness(event: String, properties: NSDictionary) throws -> URLRequest? {
func witness(event: String, properties: [String: Any]) 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
Expand Down
4 changes: 2 additions & 2 deletions Source/Core/LocalStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ final class LocalStorage: NSObject {
}

func getTargeting() -> OptableTargeting? {
guard let targetingData = UserDefaults.standard.object(forKey: targetingDataKey) as? NSDictionary else {
guard let targetingData = UserDefaults.standard.object(forKey: targetingDataKey) as? [String: Any] else {
return nil
}
let optableTargeting = OptableTargeting(
optableTargeting: targetingData,
gamTargetingKeywords: UserDefaults.standard.object(forKey: gamTargetingKeywordsKey) as? NSDictionary,
gamTargetingKeywords: UserDefaults.standard.object(forKey: gamTargetingKeywordsKey) as? [String: Any],
ortb2: UserDefaults.standard.string(forKey: ortb2Key)
)
return optableTargeting
Expand Down
115 changes: 65 additions & 50 deletions Source/OptableSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ import Foundation

// 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.
OptableDelegate enables Objective-C and Swift apps to receive results via delegate callbacks.

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.)
- Ok callbacks:
- identifyOk and witnessOk receive an HTTPURLResponse on success.
- targetingOk and profileOk receive an OptableTargeting result on success.
- Err callbacks:
- All Err methods receive an NSError describing the failure.

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.
Note for Objective-C callers: delegate-style APIs are exposed as throwing @objc methods.
If a synchronous error occurs while preparing a request, the method sets the passed NSError**
and does not invoke the delegate callbacks.
*/
@objc
public protocol OptableDelegate {
Expand All @@ -37,11 +38,11 @@ public protocol OptableDelegate {
/**
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.
An instance of OptableSDK refers to an Optable Sandbox specified by values in OptableConfig provided to the initializer.

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.
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 {
Expand All @@ -51,7 +52,7 @@ public class OptableSDK: NSObject {
let config: OptableConfig
let api: EdgeAPI

/// `OptableSDK` returns an instance of the SDK configured to use the sandbox specified by `OptableConfig`:
/// Initializes the SDK with the provided OptableConfig. On iOS 14+, requests tracking authorization unless skipAdvertisingIdDetection is true.
@objc
public init(config: OptableConfig) {
self.config = config
Expand Down Expand Up @@ -81,13 +82,20 @@ public class OptableSDK: NSObject {
// 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.
identify(ids, completion) calls the Optable Sandbox Identify API with the provided identifiers.

On completion, the handler receives:
- .success(HTTPURLResponse) on success
- .failure(Error) on failure

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.
Example:
```swift
// Example
optableSDK.identify(.init(emailAddress: "example@example.com", phoneNumber: "1234567890"), completion)
// Swift
try optableSDK.identify(
[.emailAddress("example@example.com"), .phoneNumber("1234567890")]
) { result in
// handle result
}
```
*/
func identify(_ ids: [OptableIdentifier], _ completion: @escaping (Result<HTTPURLResponse, Error>) -> Void) throws {
Expand All @@ -98,7 +106,7 @@ public extension OptableSDK {
/**
This is the Swift Concurrency compatible version of the `identify(ids, completion)` API.

Instead of completion callbacks, function have to be awaited.
Instead of completion callbacks, results are returned via async/await.
*/
@available(iOS 13.0, *)
func identify(_ ids: [OptableIdentifier]) async throws -> HTTPURLResponse {
Expand Down Expand Up @@ -134,13 +142,15 @@ public extension OptableSDK {
// 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.
targeting(ids?, completion) calls the Optable Sandbox Targeting API and returns key-value targeting data
for the current user/device/app. You may optionally supply identifiers to enrich the request.

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 completion, the handler receives:
- .success(OptableTargeting) on success
- .failure(Error) 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().
On success, the result is cached in client storage. You can read it using targetingFromCache()
and clear it using targetingClearCache().
*/
func targeting(_ ids: [OptableIdentifier]? = nil, completion: @escaping (Result<OptableTargeting, Error>) -> Void) throws {
try _targeting(ids: ids, completion: completion)
Expand All @@ -162,7 +172,7 @@ public extension OptableSDK {
/**
This is the Swift Concurrency compatible version of the `targeting(completion)` API.

Instead of completion callbacks, function have to be awaited.
Instead of completion callbacks, results are returned via async/await.
*/
@available(iOS 13.0, *)
func targeting(_ ids: [OptableIdentifier]? = nil) async throws -> OptableTargeting {
Expand Down Expand Up @@ -198,23 +208,26 @@ public extension OptableSDK {
// 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.
witness(event, properties, completion) calls the Optable Sandbox Witness API to log an event
(for example, "app.screenView" or "ui.buttonPressed") with the provided properties.
These events can later be 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.
On completion, the handler receives:
- .success(HTTPURLResponse) on success
- .failure(Error) on failure
*/
func witness(event: String, properties: NSDictionary, _ completion: @escaping (Result<HTTPURLResponse, Error>) -> Void) throws {
func witness(event: String, properties: [String: Any], _ completion: @escaping (Result<HTTPURLResponse, Error>) -> 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.
Instead of completion callbacks, results are returned via async/await.
*/
@available(iOS 13.0, *)
func witness(event: String, properties: NSDictionary) async throws -> HTTPURLResponse {
func witness(event: String, properties: [String: Any]) async throws -> HTTPURLResponse {
return try await withCheckedThrowingContinuation({ [unowned self] continuation in
do {
try self._witness(event: event, properties: properties, completion: { continuation.resume(with: $0) })
Expand All @@ -231,7 +244,7 @@ public extension OptableSDK {
Instead of completion callbacks, delegate methods are called.
*/
@objc
func witness(event: String, properties: NSDictionary) throws {
func witness(event: String, properties: [String: Any]) throws {
try self.witness(event: event, properties: properties) { result in
switch result {
case let .success(response):
Expand All @@ -246,23 +259,27 @@ public extension OptableSDK {
// 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.
profile(traits, id, neighbors, completion) calls the Optable Sandbox Profile API to associate the provided
traits (key-value pairs) with the user/device. You can optionally include a specific id and neighbor ids.

On completion, the handler receives:
- .success(OptableTargeting) on success
- .failure(Error) on failure

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.
The resulting OptableTargeting is also cached for targetingFromCache().
*/
func profile(traits: NSDictionary, id: String? = nil, neighbors: [String]? = nil, _ completion: @escaping (Result<OptableTargeting, Error>) -> Void) throws {
func profile(traits: [String: Any], id: String? = nil, neighbors: [String]? = nil, _ completion: @escaping (Result<OptableTargeting, Error>) -> 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.
Instead of completion callbacks, results are returned via async/await.
*/
@available(iOS 13.0, *)
func profile(traits: NSDictionary, id: String? = nil, neighbors: [String]? = nil) async throws -> OptableTargeting {
func profile(traits: [String: Any], id: String? = nil, neighbors: [String]? = nil) async throws -> OptableTargeting {
return try await withCheckedThrowingContinuation({ [unowned self] continuation in
do {
try self._profile(traits: traits, id: id, neighbors: neighbors, completion: { continuation.resume(with: $0) })
Expand All @@ -279,7 +296,7 @@ public extension OptableSDK {
Instead of completion callbacks, delegate methods are called.
*/
@objc
func profile(traits: NSDictionary, id: String? = nil, neighbors: [String]? = nil) throws {
func profile(traits: [String: Any], id: String? = nil, neighbors: [String]? = nil) throws {
try _profile(traits: traits, id: id, neighbors: neighbors, completion: { result in
switch result {
case let .success(response):
Expand All @@ -293,15 +310,13 @@ public extension OptableSDK {

// MARK: - Identify from URL
public extension OptableSDK {
///
/// 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.
///
/**
Attempts to extract an "oeid" query parameter from the given URL string and, if present,
calls identify with that identifier.

Use this when handling incoming universal links that may contain an "oeid"
(for example, SHA256(lowercased(email)) embedded in campaign links).
*/
@objc
func tryIdentifyFromURL(_ urlString: String) throws {
let eidStr = OptableIdentifierEncoder.eidFromURL(urlString)
Expand Down Expand Up @@ -378,7 +393,7 @@ private extension OptableSDK {
}).resume()
}

func _witness(event: String, properties: NSDictionary, completion: @escaping (Result<HTTPURLResponse, Error>) -> Void) throws {
func _witness(event: String, properties: [String: Any], completion: @escaping (Result<HTTPURLResponse, Error>) -> Void) throws {
guard let request = try api.witness(event: event, properties: properties) else {
throw OptableError.witness("Failed to create witness request")
}
Expand All @@ -401,7 +416,7 @@ private extension OptableSDK {
}).resume()
}

func _profile(traits: NSDictionary, id: String?, neighbors: [String]?, completion: @escaping (Result<OptableTargeting, Error>) -> Void) throws {
func _profile(traits: [String: Any], id: String?, neighbors: [String]?, completion: @escaping (Result<OptableTargeting, Error>) -> Void) throws {
guard let request = try api.profile(traits: traits, id: id, neighbors: neighbors) else {
throw OptableError.profile("Failed to create profile request")
}
Expand Down Expand Up @@ -495,8 +510,8 @@ private extension OptableSDK {
let optableTargetingData = try JSONSerialization.jsonObject(with: responseData, options: [])
let optableTargetingDict: NSMutableDictionary = ((optableTargetingData as? NSDictionary)?.mutableCopy() as? NSMutableDictionary) ?? NSMutableDictionary()
let optableTargeting = OptableTargeting(
optableTargeting: optableTargetingDict,
gamTargetingKeywords: OptableSDK.generateGAMTargetingKeywords(from: optableTargetingDict),
optableTargeting: optableTargetingDict as? [String: Any] ?? [:],
gamTargetingKeywords: OptableSDK.generateGAMTargetingKeywords(from: optableTargetingDict) as? [String : Any],
ortb2: OptableSDK.generateORTB2Config(from: optableTargetingDict)
)

Expand Down
6 changes: 3 additions & 3 deletions Source/Public/OptableTargeting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import Foundation

@objcMembers
public class OptableTargeting: NSObject {
public let targetingData: NSDictionary
public let gamTargetingKeywords: NSDictionary?
public let targetingData: [String: Any]
public let gamTargetingKeywords: [String: Any]?
public let ortb2: String?

public init(optableTargeting: NSDictionary, gamTargetingKeywords: NSDictionary? = nil, ortb2: String? = nil) {
public init(optableTargeting: [String: Any], gamTargetingKeywords: [String: Any]? = nil, ortb2: String? = nil) {
self.targetingData = optableTargeting
self.gamTargetingKeywords = gamTargetingKeywords
self.ortb2 = ortb2
Expand Down
12 changes: 6 additions & 6 deletions Tests/Integration/OptableSDKTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ class OptableSDKTests: XCTestCase {
@available(iOS 13.0, *)
func test_target_async() async throws {
let response = try await sdk.targeting([.emailAddress("test@test.com")])
XCTAssert(response.targetingData.allKeys.isEmpty == false)
XCTAssert(response.targetingData.keys.isEmpty == false)
}

func test_target_callback() throws {
let expectation = expectation(description: "target-callback-expectation")
try sdk.targeting([.emailAddress("test@test.com")], completion: { result in
switch result {
case let .success(response):
XCTAssert(response.targetingData.allKeys.isEmpty == false)
XCTAssert(response.targetingData.keys.isEmpty == false)
case let .failure(failure):
XCTFail("Expected success, got error: \(failure)")
}
Expand Down Expand Up @@ -109,15 +109,15 @@ class OptableSDKTests: XCTestCase {
@available(iOS 13.0, *)
func test_profile_async() async throws {
let response = try await sdk.profile(traits: ["integration-test-profile": "integration-test-profile-value"])
XCTAssert(response.targetingData.allKeys.isEmpty == false)
XCTAssert(response.targetingData.keys.isEmpty == false)
}

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.targetingData.allKeys.isEmpty == false)
XCTAssert(response.targetingData.keys.isEmpty == false)
case let .failure(failure):
XCTFail("Expected success, got error: \(failure)")
}
Expand Down Expand Up @@ -146,7 +146,7 @@ extension OptableSDKTests: OptableDelegate {
}

func profileOk(_ result: OptableTargeting) {
XCTAssert(result.targetingData.allKeys.isEmpty == false)
XCTAssert(result.targetingData.keys.isEmpty == false)
profileExpectation.fulfill()
}

Expand All @@ -156,7 +156,7 @@ extension OptableSDKTests: OptableDelegate {
}

func targetingOk(_ result: OptableTargeting) {
XCTAssert(result.targetingData.allKeys.isEmpty == false)
XCTAssert(result.targetingData.keys.isEmpty == false)
targetExpectation.fulfill()
}

Expand Down
Loading
Loading