diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml
index dacf0f0..b8f3068 100644
--- a/.github/workflows/test-pull-request.yml
+++ b/.github/workflows/test-pull-request.yml
@@ -19,8 +19,14 @@ jobs:
- name: Lint code
run: swiftlint lint --config .swiftlint.yml --reporter github-actions-logging
- - name: Build
- run: xcodebuild -scheme UID2 -sdk iphonesimulator16.2 -destination "OS=16.2,name=iPhone 14"
+ - name: Build for iOS
+ run: xcodebuild -scheme UID2 -destination "generic/platform=iOS"
+
+ - name: Build for tvOS
+ run: xcodebuild -scheme UID2 -destination "generic/platform=tvOS"
- name: Run unit tests
run: xcodebuild test -scheme UID2Tests -sdk iphonesimulator16.2 -destination "OS=16.2,name=iPhone 14"
+
+ - name: Run unit tests on tvOS
+ run: xcodebuild test -scheme UID2Tests -sdk appletvsimulator16.1 -destination "OS=16.1,name=Apple TV"
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/UID2.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/UID2.xcscheme
index 3ce251a..3792a46 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/UID2.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/UID2.xcscheme
@@ -28,6 +28,16 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
+
+
+
+
(Data, Int)
diff --git a/Sources/UID2/Networking/RefreshRequest.swift b/Sources/UID2/Networking/RefreshRequest.swift
new file mode 100644
index 0000000..79b685a
--- /dev/null
+++ b/Sources/UID2/Networking/RefreshRequest.swift
@@ -0,0 +1,23 @@
+//
+// RefreshRequest.swift
+//
+//
+// Created by Dave Snabel-Caunt on 24/04/2024.
+//
+
+import Foundation
+
+extension Request {
+ static func refresh(
+ token: String
+ ) -> Request {
+ .init(
+ path: "/v2/token/refresh",
+ method: .post,
+ body: Data(token.utf8),
+ headers: [
+ "Content-Type": "application/x-www-form-urlencoded"
+ ]
+ )
+ }
+}
diff --git a/Sources/UID2/Networking/Request.swift b/Sources/UID2/Networking/Request.swift
new file mode 100644
index 0000000..335dae7
--- /dev/null
+++ b/Sources/UID2/Networking/Request.swift
@@ -0,0 +1,35 @@
+//
+// Request.swift
+//
+//
+// Created by Dave Snabel-Caunt on 09/04/2024.
+//
+
+import Foundation
+
+enum Method: String {
+ case get = "GET"
+ case post = "POST"
+}
+
+struct Request {
+ var method: Method
+ var path: String
+ var queryItems: [URLQueryItem]
+ var body: Data?
+ var headers: [String: String]
+
+ init(
+ path: String,
+ method: Method = .get,
+ queryItems: [URLQueryItem] = [],
+ body: Data? = nil,
+ headers: [String: String] = [:]
+ ) {
+ self.path = path
+ self.method = method
+ self.queryItems = queryItems
+ self.body = body
+ self.headers = headers
+ }
+}
diff --git a/Sources/UID2/UID2Client.swift b/Sources/UID2/UID2Client.swift
index 24deb87..d141264 100644
--- a/Sources/UID2/UID2Client.swift
+++ b/Sources/UID2/UID2Client.swift
@@ -7,7 +7,6 @@
import Foundation
-@available(iOS 13.0, *)
internal final class UID2Client {
private let uid2APIURL: String
@@ -16,56 +15,75 @@ internal final class UID2Client {
init(uid2APIURL: String, sdkVersion: String, _ session: NetworkSession = URLSession.shared) {
self.uid2APIURL = uid2APIURL
+ #if os(tvOS)
+ self.clientVersion = "tvos-\(sdkVersion)"
+ #else
self.clientVersion = "ios-\(sdkVersion)"
+ #endif
self.session = session
}
func refreshIdentity(refreshToken: String, refreshResponseKey: String) async throws -> RefreshAPIPackage {
- var components = URLComponents(string: uid2APIURL)
- components?.path = "/v2/token/refresh"
-
- guard let urlPath = components?.url?.absoluteString,
- let url = URL(string: urlPath) else {
- throw UID2Error.urlGeneration
- }
-
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.addValue(clientVersion, forHTTPHeaderField: "X-UID2-Client-Version")
- request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
- request.httpBody = refreshToken.data(using: .utf8)
-
- let dataResponse = try await session.loadData(for: request)
- let data = dataResponse.0
- let statusCode = dataResponse.1
-
- let decoder = JSONDecoder()
- decoder.keyDecodingStrategy = .convertFromSnakeCase
-
- // Only Decrypt If HTTP Status is 200 (Success or Opt Out)
- if statusCode != 200 {
- do {
- let tokenResponse = try decoder.decode(RefreshTokenResponse.self, from: data)
- throw UID2Error.refreshTokenServer(status: tokenResponse.status, message: tokenResponse.message)
- } catch {
- throw UID2Error.refreshTokenServerDecoding(httpStatus: statusCode, message: error.localizedDescription)
- }
- }
-
- // Decrypt Data Envelop
- // https://github.com/UnifiedID2/uid2docs/blob/main/api/v2/encryption-decryption.md
- guard let payloadData = DataEnvelope.decrypt(refreshResponseKey, data, true) else {
- throw UID2Error.decryptPayloadData
- }
-
- let tokenResponse = try decoder.decode(RefreshTokenResponse.self, from: payloadData)
-
- guard let refreshAPIPackage = tokenResponse.toRefreshAPIPackage() else {
- throw UID2Error.refreshResponseToRefreshAPIPackage
+ let request = Request.refresh(token: refreshToken)
+ let (data, statusCode) = try await execute(request)
+
+ let decoder = JSONDecoder()
+ decoder.keyDecodingStrategy = .convertFromSnakeCase
+
+ // Only Decrypt If HTTP Status is 200 (Success or Opt Out)
+ if statusCode != 200 {
+ do {
+ let tokenResponse = try decoder.decode(RefreshTokenResponse.self, from: data)
+ throw UID2Error.refreshTokenServer(status: tokenResponse.status, message: tokenResponse.message)
+ } catch {
+ throw UID2Error.refreshTokenServerDecoding(httpStatus: statusCode, message: error.localizedDescription)
}
-
- return refreshAPIPackage
}
+ // Decrypt Data Envelop
+ // https://github.com/UnifiedID2/uid2docs/blob/main/api/v2/encryption-decryption.md
+ guard let payloadData = DataEnvelope.decrypt(refreshResponseKey, data, true) else {
+ throw UID2Error.decryptPayloadData
+ }
+
+ let tokenResponse = try decoder.decode(RefreshTokenResponse.self, from: payloadData)
+
+ guard let refreshAPIPackage = tokenResponse.toRefreshAPIPackage() else {
+ throw UID2Error.refreshResponseToRefreshAPIPackage
+ }
+
+ return refreshAPIPackage
+ }
+
+ // MARK: - Request Execution
+
+ internal func urlRequest(
+ _ request: Request,
+ baseURL: URL
+ ) -> URLRequest {
+ var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)!
+ urlComponents.path = request.path
+ urlComponents.queryItems = request.queryItems.isEmpty ? nil : request.queryItems
+
+ var urlRequest = URLRequest(url: urlComponents.url!)
+ urlRequest.httpMethod = request.method.rawValue
+ if request.method == .post {
+ urlRequest.httpBody = request.body
+ }
+
+ request.headers.forEach { field, value in
+ urlRequest.addValue(value, forHTTPHeaderField: field)
+ }
+ urlRequest.addValue(clientVersion, forHTTPHeaderField: "X-UID2-Client-Version")
+ return urlRequest
+ }
+
+ private func execute(_ request: Request) async throws -> (Data, Int) {
+ let urlRequest = urlRequest(
+ request,
+ baseURL: URL(string: uid2APIURL)!
+ )
+ return try await session.loadData(for: urlRequest)
+ }
}
diff --git a/Sources/UID2/UID2Error.swift b/Sources/UID2/UID2Error.swift
index 97ebdf2..70bb4b6 100644
--- a/Sources/UID2/UID2Error.swift
+++ b/Sources/UID2/UID2Error.swift
@@ -9,7 +9,6 @@ import Foundation
/// UID2 Specifc Errors
-@available(iOS 13.0, *)
enum UID2Error: Error {
/// Unable to decrypt Payload Data
diff --git a/Sources/UID2/UID2Manager.swift b/Sources/UID2/UID2Manager.swift
index bcddad5..e1e46b8 100644
--- a/Sources/UID2/UID2Manager.swift
+++ b/Sources/UID2/UID2Manager.swift
@@ -8,7 +8,6 @@
import Combine
import Foundation
-@available(iOS 13.0, *)
public final actor UID2Manager {
/// Singleton access point for UID2Manager
diff --git a/Tests/UID2Tests/RefreshRequestTests.swift b/Tests/UID2Tests/RefreshRequestTests.swift
new file mode 100644
index 0000000..4d1f105
--- /dev/null
+++ b/Tests/UID2Tests/RefreshRequestTests.swift
@@ -0,0 +1,45 @@
+//
+// RefreshRequestTests.swift
+//
+//
+// Created by Dave Snabel-Caunt on 24/04/2024.
+//
+
+import XCTest
+@testable import UID2
+
+final class RefreshRequestTests: XCTestCase {
+
+ func testRequest() async throws {
+ let request = Request.refresh(token: "im-a-refresh-token")
+ let client = UID2Client(
+ uid2APIURL: "https://prod.uidapi.com",
+ sdkVersion: "1.2.3"
+ )
+ let urlRequest = client.urlRequest(request, baseURL: URL(string: "https://prod.uidapi.com")!)
+
+ var expected = URLRequest(url: URL(string: "https://prod.uidapi.com/v2/token/refresh")!)
+ expected.httpMethod = "POST"
+ expected.httpBody = Data("im-a-refresh-token".utf8)
+
+#if os(tvOS)
+ expected.allHTTPHeaderFields = [
+ "Content-Type": "application/x-www-form-urlencoded",
+ "X-UID2-Client-Version": "tvos-1.2.3"
+ ]
+#else
+ expected.allHTTPHeaderFields = [
+ "Content-Type": "application/x-www-form-urlencoded",
+ "X-UID2-Client-Version": "ios-1.2.3"
+ ]
+#endif
+ XCTAssertEqual(urlRequest, expected)
+
+ // The above equality test doesn't print useful information on failure, so
+ // it's useful to check properties below for diagnostics
+ XCTAssertEqual(urlRequest.url, expected.url)
+ XCTAssertEqual(urlRequest.httpMethod, expected.httpMethod)
+ XCTAssertEqual(urlRequest.httpBody, expected.httpBody)
+ XCTAssertEqual(urlRequest.allHTTPHeaderFields, expected.allHTTPHeaderFields)
+ }
+}
diff --git a/Tests/UID2Tests/RefreshTokenAPITests.swift b/Tests/UID2Tests/RefreshTokenAPITests.swift
index 9afb3fa..5e7c70c 100644
--- a/Tests/UID2Tests/RefreshTokenAPITests.swift
+++ b/Tests/UID2Tests/RefreshTokenAPITests.swift
@@ -26,7 +26,11 @@ final class RefreshTokenAPITests: XCTestCase {
}
// Load UID2Client Mocked
- let client = UID2Client(uid2APIURL: "", sdkVersion: "TEST", MockNetworkSession("refresh-token-200-success-encrypted", "txt"))
+ let client = UID2Client(
+ uid2APIURL: "https://prod.uidapi.com",
+ sdkVersion: "TEST",
+ MockNetworkSession("refresh-token-200-success-encrypted", "txt")
+ )
// Call RefreshToken using refreshToken and refreshResponseKey from Step 1 to decrypt
let refreshToken = try await client.refreshIdentity(refreshToken: generateToken.refreshToken,
@@ -65,7 +69,11 @@ final class RefreshTokenAPITests: XCTestCase {
}
// Load UID2Client Mocked
- let client = UID2Client(uid2APIURL: "", sdkVersion: "TEST", MockNetworkSession("refresh-token-200-optout-encrypted", "txt"))
+ let client = UID2Client(
+ uid2APIURL: "https://prod.uidapi.com",
+ sdkVersion: "TEST",
+ MockNetworkSession("refresh-token-200-optout-encrypted", "txt")
+ )
// Call RefreshToken using refreshToken and refreshResponseKey from Step 1 to decrypt
let refreshToken = try await client.refreshIdentity(refreshToken: generateToken.refreshToken,
@@ -85,8 +93,12 @@ final class RefreshTokenAPITests: XCTestCase {
do {
// Load UID2Client Mocked
- let client = UID2Client(uid2APIURL: "", sdkVersion: "TEST", MockNetworkSession("refresh-token-400-client-error", "json", 400))
-
+ let client = UID2Client(
+ uid2APIURL: "https://prod.uidapi.com",
+ sdkVersion: "TEST",
+ MockNetworkSession("refresh-token-400-client-error", "json", 400)
+ )
+
// Call RefreshToken using refreshToken and refreshResponseKey from Step 1 to decrypt
let _ = try await client.refreshIdentity(refreshToken: "token", refreshResponseKey: "key")
XCTFail("refreshUID2Token() did not throw an error.")
@@ -111,7 +123,11 @@ final class RefreshTokenAPITests: XCTestCase {
do {
// Load UID2Client Mocked
- let client = UID2Client(uid2APIURL: "", sdkVersion: "TEST", MockNetworkSession("refresh-token-400-invalid-token", "json", 400))
+ let client = UID2Client(
+ uid2APIURL: "https://prod.uidapi.com",
+ sdkVersion: "TEST",
+ MockNetworkSession("refresh-token-400-invalid-token", "json", 400)
+ )
// Call RefreshToken using refreshToken and refreshResponseKey from Step 1 to decrypt
let _ = try await client.refreshIdentity(refreshToken: "token", refreshResponseKey: "key")
@@ -137,7 +153,11 @@ final class RefreshTokenAPITests: XCTestCase {
do {
// Load UID2Client Mocked
- let client = UID2Client(uid2APIURL: "", sdkVersion: "TEST", MockNetworkSession("refresh-token-401-unauthorized", "json", 401))
+ let client = UID2Client(
+ uid2APIURL: "https://prod.uidapi.com",
+ sdkVersion: "TEST",
+ MockNetworkSession("refresh-token-401-unauthorized", "json", 401)
+ )
// Call RefreshToken using refreshToken and refreshResponseKey from Step 1 to decrypt
let _ = try await client.refreshIdentity(refreshToken: "token", refreshResponseKey: "key")