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+
}
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
}

0 commit comments

Comments
 (0)