Skip to content

Commit 05fdfff

Browse files
authored
Add tvOS support (#23)
* Add tvOS support - Declare tvOS support in Package.swift - Remove unnecessary available annotations - Fix test reference for UID2 target * Add tests and Request type
1 parent d745cbb commit 05fdfff

File tree

14 files changed

+212
-58
lines changed

14 files changed

+212
-58
lines changed

.github/workflows/test-pull-request.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ jobs:
1919
- name: Lint code
2020
run: swiftlint lint --config .swiftlint.yml --reporter github-actions-logging
2121

22-
- name: Build
23-
run: xcodebuild -scheme UID2 -sdk iphonesimulator16.2 -destination "OS=16.2,name=iPhone 14"
22+
- name: Build for iOS
23+
run: xcodebuild -scheme UID2 -destination "generic/platform=iOS"
24+
25+
- name: Build for tvOS
26+
run: xcodebuild -scheme UID2 -destination "generic/platform=tvOS"
2427

2528
- name: Run unit tests
2629
run: xcodebuild test -scheme UID2Tests -sdk iphonesimulator16.2 -destination "OS=16.2,name=iPhone 14"
30+
31+
- name: Run unit tests on tvOS
32+
run: xcodebuild test -scheme UID2Tests -sdk appletvsimulator16.1 -destination "OS=16.1,name=Apple TV"

.swiftpm/xcode/xcshareddata/xcschemes/UID2.xcscheme

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@
2828
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
2929
shouldUseLaunchSchemeArgsEnv = "YES">
3030
<Testables>
31+
<TestableReference
32+
skipped = "NO">
33+
<BuildableReference
34+
BuildableIdentifier = "primary"
35+
BlueprintIdentifier = "UID2Tests"
36+
BuildableName = "UID2Tests"
37+
BlueprintName = "UID2Tests"
38+
ReferencedContainer = "container:">
39+
</BuildableReference>
40+
</TestableReference>
3141
</Testables>
3242
</TestAction>
3343
<LaunchAction

Development/UID2SDKDevelopmentApp/UID2SDKDevelopmentApp/Networking/AppUID2Client.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import Foundation
1010
import Security
1111
import UID2
1212

13-
@available(iOS 13.0, *)
1413
internal final class AppUID2Client {
1514

1615
enum RequestTypes: String, CaseIterable {

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ let package = Package(
77
name: "UID2",
88
defaultLocalization: "en",
99
platforms: [
10-
.iOS(.v13)
10+
.iOS(.v13),
11+
.tvOS(.v13)
1112
],
1213
products: [
1314
.library(

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ A framework for integrating [UID2](https://github.com/IABTechLab/uid2docs) into
3030
| Platform | Minimum target | Swift Version |
3131
| --- | --- | --- |
3232
| iOS | 13.0+ | 5.0+ |
33+
| tvOS | 13.0+ | 5.0+ |
3334

3435
## Development
3536

Sources/UID2/KeychainManager.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import Foundation
66
import Security
77

88
/// Securely manages data in the Keychain
9-
@available(iOS 13.0, *)
109
internal final class KeychainManager {
1110

1211
/// Singleton access point for KeychainManager

Sources/UID2/Networking/NetworkSession.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import Foundation
99

1010
/// Common interface for networking and unit testing
11-
@available(iOS 13.0, *)
1211
protocol NetworkSession {
1312

1413
func loadData(for request: URLRequest) async throws -> (Data, Int)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// RefreshRequest.swift
3+
//
4+
//
5+
// Created by Dave Snabel-Caunt on 24/04/2024.
6+
//
7+
8+
import Foundation
9+
10+
extension Request {
11+
static func refresh(
12+
token: String
13+
) -> Request {
14+
.init(
15+
path: "/v2/token/refresh",
16+
method: .post,
17+
body: Data(token.utf8),
18+
headers: [
19+
"Content-Type": "application/x-www-form-urlencoded"
20+
]
21+
)
22+
}
23+
}

Sources/UID2/Networking/Request.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// Request.swift
3+
//
4+
//
5+
// Created by Dave Snabel-Caunt on 09/04/2024.
6+
//
7+
8+
import Foundation
9+
10+
enum Method: String {
11+
case get = "GET"
12+
case post = "POST"
13+
}
14+
15+
struct Request {
16+
var method: Method
17+
var path: String
18+
var queryItems: [URLQueryItem]
19+
var body: Data?
20+
var headers: [String: String]
21+
22+
init(
23+
path: String,
24+
method: Method = .get,
25+
queryItems: [URLQueryItem] = [],
26+
body: Data? = nil,
27+
headers: [String: String] = [:]
28+
) {
29+
self.path = path
30+
self.method = method
31+
self.queryItems = queryItems
32+
self.body = body
33+
self.headers = headers
34+
}
35+
}

Sources/UID2/UID2Client.swift

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import Foundation
99

10-
@available(iOS 13.0, *)
1110
internal final class UID2Client {
1211

1312
private let uid2APIURL: String
@@ -16,56 +15,75 @@ internal final class UID2Client {
1615

1716
init(uid2APIURL: String, sdkVersion: String, _ session: NetworkSession = URLSession.shared) {
1817
self.uid2APIURL = uid2APIURL
18+
#if os(tvOS)
19+
self.clientVersion = "tvos-\(sdkVersion)"
20+
#else
1921
self.clientVersion = "ios-\(sdkVersion)"
22+
#endif
2023
self.session = session
2124
}
2225

2326
func refreshIdentity(refreshToken: String, refreshResponseKey: String) async throws -> RefreshAPIPackage {
2427

25-
var components = URLComponents(string: uid2APIURL)
26-
components?.path = "/v2/token/refresh"
27-
28-
guard let urlPath = components?.url?.absoluteString,
29-
let url = URL(string: urlPath) else {
30-
throw UID2Error.urlGeneration
31-
}
32-
33-
var request = URLRequest(url: url)
34-
request.httpMethod = "POST"
35-
request.addValue(clientVersion, forHTTPHeaderField: "X-UID2-Client-Version")
36-
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
37-
request.httpBody = refreshToken.data(using: .utf8)
38-
39-
let dataResponse = try await session.loadData(for: request)
40-
let data = dataResponse.0
41-
let statusCode = dataResponse.1
42-
43-
let decoder = JSONDecoder()
44-
decoder.keyDecodingStrategy = .convertFromSnakeCase
45-
46-
// Only Decrypt If HTTP Status is 200 (Success or Opt Out)
47-
if statusCode != 200 {
48-
do {
49-
let tokenResponse = try decoder.decode(RefreshTokenResponse.self, from: data)
50-
throw UID2Error.refreshTokenServer(status: tokenResponse.status, message: tokenResponse.message)
51-
} catch {
52-
throw UID2Error.refreshTokenServerDecoding(httpStatus: statusCode, message: error.localizedDescription)
53-
}
54-
}
55-
56-
// Decrypt Data Envelop
57-
// https://github.com/UnifiedID2/uid2docs/blob/main/api/v2/encryption-decryption.md
58-
guard let payloadData = DataEnvelope.decrypt(refreshResponseKey, data, true) else {
59-
throw UID2Error.decryptPayloadData
60-
}
61-
62-
let tokenResponse = try decoder.decode(RefreshTokenResponse.self, from: payloadData)
63-
64-
guard let refreshAPIPackage = tokenResponse.toRefreshAPIPackage() else {
65-
throw UID2Error.refreshResponseToRefreshAPIPackage
28+
let request = Request.refresh(token: refreshToken)
29+
let (data, statusCode) = try await execute(request)
30+
31+
let decoder = JSONDecoder()
32+
decoder.keyDecodingStrategy = .convertFromSnakeCase
33+
34+
// Only Decrypt If HTTP Status is 200 (Success or Opt Out)
35+
if statusCode != 200 {
36+
do {
37+
let tokenResponse = try decoder.decode(RefreshTokenResponse.self, from: data)
38+
throw UID2Error.refreshTokenServer(status: tokenResponse.status, message: tokenResponse.message)
39+
} catch {
40+
throw UID2Error.refreshTokenServerDecoding(httpStatus: statusCode, message: error.localizedDescription)
6641
}
67-
68-
return refreshAPIPackage
6942
}
7043

44+
// Decrypt Data Envelop
45+
// https://github.com/UnifiedID2/uid2docs/blob/main/api/v2/encryption-decryption.md
46+
guard let payloadData = DataEnvelope.decrypt(refreshResponseKey, data, true) else {
47+
throw UID2Error.decryptPayloadData
48+
}
49+
50+
let tokenResponse = try decoder.decode(RefreshTokenResponse.self, from: payloadData)
51+
52+
guard let refreshAPIPackage = tokenResponse.toRefreshAPIPackage() else {
53+
throw UID2Error.refreshResponseToRefreshAPIPackage
54+
}
55+
56+
return refreshAPIPackage
57+
}
58+
59+
// MARK: - Request Execution
60+
61+
internal func urlRequest(
62+
_ request: Request,
63+
baseURL: URL
64+
) -> URLRequest {
65+
var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)!
66+
urlComponents.path = request.path
67+
urlComponents.queryItems = request.queryItems.isEmpty ? nil : request.queryItems
68+
69+
var urlRequest = URLRequest(url: urlComponents.url!)
70+
urlRequest.httpMethod = request.method.rawValue
71+
if request.method == .post {
72+
urlRequest.httpBody = request.body
73+
}
74+
75+
request.headers.forEach { field, value in
76+
urlRequest.addValue(value, forHTTPHeaderField: field)
77+
}
78+
urlRequest.addValue(clientVersion, forHTTPHeaderField: "X-UID2-Client-Version")
79+
return urlRequest
80+
}
81+
82+
private func execute(_ request: Request) async throws -> (Data, Int) {
83+
let urlRequest = urlRequest(
84+
request,
85+
baseURL: URL(string: uid2APIURL)!
86+
)
87+
return try await session.loadData(for: urlRequest)
88+
}
7189
}

Sources/UID2/UID2Error.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Foundation
99

1010
/// UID2 Specifc Errors
1111

12-
@available(iOS 13.0, *)
1312
enum UID2Error: Error {
1413

1514
/// Unable to decrypt Payload Data

Sources/UID2/UID2Manager.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import Combine
99
import Foundation
1010

11-
@available(iOS 13.0, *)
1211
public final actor UID2Manager {
1312

1413
/// Singleton access point for UID2Manager
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// RefreshRequestTests.swift
3+
//
4+
//
5+
// Created by Dave Snabel-Caunt on 24/04/2024.
6+
//
7+
8+
import XCTest
9+
@testable import UID2
10+
11+
final class RefreshRequestTests: XCTestCase {
12+
13+
func testRequest() async throws {
14+
let request = Request.refresh(token: "im-a-refresh-token")
15+
let client = UID2Client(
16+
uid2APIURL: "https://prod.uidapi.com",
17+
sdkVersion: "1.2.3"
18+
)
19+
let urlRequest = client.urlRequest(request, baseURL: URL(string: "https://prod.uidapi.com")!)
20+
21+
var expected = URLRequest(url: URL(string: "https://prod.uidapi.com/v2/token/refresh")!)
22+
expected.httpMethod = "POST"
23+
expected.httpBody = Data("im-a-refresh-token".utf8)
24+
25+
#if os(tvOS)
26+
expected.allHTTPHeaderFields = [
27+
"Content-Type": "application/x-www-form-urlencoded",
28+
"X-UID2-Client-Version": "tvos-1.2.3"
29+
]
30+
#else
31+
expected.allHTTPHeaderFields = [
32+
"Content-Type": "application/x-www-form-urlencoded",
33+
"X-UID2-Client-Version": "ios-1.2.3"
34+
]
35+
#endif
36+
XCTAssertEqual(urlRequest, expected)
37+
38+
// The above equality test doesn't print useful information on failure, so
39+
// it's useful to check properties below for diagnostics
40+
XCTAssertEqual(urlRequest.url, expected.url)
41+
XCTAssertEqual(urlRequest.httpMethod, expected.httpMethod)
42+
XCTAssertEqual(urlRequest.httpBody, expected.httpBody)
43+
XCTAssertEqual(urlRequest.allHTTPHeaderFields, expected.allHTTPHeaderFields)
44+
}
45+
}

Tests/UID2Tests/RefreshTokenAPITests.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ final class RefreshTokenAPITests: XCTestCase {
2626
}
2727

2828
// Load UID2Client Mocked
29-
let client = UID2Client(uid2APIURL: "", sdkVersion: "TEST", MockNetworkSession("refresh-token-200-success-encrypted", "txt"))
29+
let client = UID2Client(
30+
uid2APIURL: "https://prod.uidapi.com",
31+
sdkVersion: "TEST",
32+
MockNetworkSession("refresh-token-200-success-encrypted", "txt")
33+
)
3034

3135
// Call RefreshToken using refreshToken and refreshResponseKey from Step 1 to decrypt
3236
let refreshToken = try await client.refreshIdentity(refreshToken: generateToken.refreshToken,
@@ -65,7 +69,11 @@ final class RefreshTokenAPITests: XCTestCase {
6569
}
6670

6771
// Load UID2Client Mocked
68-
let client = UID2Client(uid2APIURL: "", sdkVersion: "TEST", MockNetworkSession("refresh-token-200-optout-encrypted", "txt"))
72+
let client = UID2Client(
73+
uid2APIURL: "https://prod.uidapi.com",
74+
sdkVersion: "TEST",
75+
MockNetworkSession("refresh-token-200-optout-encrypted", "txt")
76+
)
6977

7078
// Call RefreshToken using refreshToken and refreshResponseKey from Step 1 to decrypt
7179
let refreshToken = try await client.refreshIdentity(refreshToken: generateToken.refreshToken,
@@ -85,8 +93,12 @@ final class RefreshTokenAPITests: XCTestCase {
8593

8694
do {
8795
// Load UID2Client Mocked
88-
let client = UID2Client(uid2APIURL: "", sdkVersion: "TEST", MockNetworkSession("refresh-token-400-client-error", "json", 400))
89-
96+
let client = UID2Client(
97+
uid2APIURL: "https://prod.uidapi.com",
98+
sdkVersion: "TEST",
99+
MockNetworkSession("refresh-token-400-client-error", "json", 400)
100+
)
101+
90102
// Call RefreshToken using refreshToken and refreshResponseKey from Step 1 to decrypt
91103
let _ = try await client.refreshIdentity(refreshToken: "token", refreshResponseKey: "key")
92104
XCTFail("refreshUID2Token() did not throw an error.")
@@ -111,7 +123,11 @@ final class RefreshTokenAPITests: XCTestCase {
111123

112124
do {
113125
// Load UID2Client Mocked
114-
let client = UID2Client(uid2APIURL: "", sdkVersion: "TEST", MockNetworkSession("refresh-token-400-invalid-token", "json", 400))
126+
let client = UID2Client(
127+
uid2APIURL: "https://prod.uidapi.com",
128+
sdkVersion: "TEST",
129+
MockNetworkSession("refresh-token-400-invalid-token", "json", 400)
130+
)
115131

116132
// Call RefreshToken using refreshToken and refreshResponseKey from Step 1 to decrypt
117133
let _ = try await client.refreshIdentity(refreshToken: "token", refreshResponseKey: "key")
@@ -137,7 +153,11 @@ final class RefreshTokenAPITests: XCTestCase {
137153

138154
do {
139155
// Load UID2Client Mocked
140-
let client = UID2Client(uid2APIURL: "", sdkVersion: "TEST", MockNetworkSession("refresh-token-401-unauthorized", "json", 401))
156+
let client = UID2Client(
157+
uid2APIURL: "https://prod.uidapi.com",
158+
sdkVersion: "TEST",
159+
MockNetworkSession("refresh-token-401-unauthorized", "json", 401)
160+
)
141161

142162
// Call RefreshToken using refreshToken and refreshResponseKey from Step 1 to decrypt
143163
let _ = try await client.refreshIdentity(refreshToken: "token", refreshResponseKey: "key")

0 commit comments

Comments
 (0)