Skip to content

Commit eb745e6

Browse files
authored
EUID Support (#57)
* Implement EUID Support * Add an Info.plist toggle for UID2/EUID * Use bundleID for EUID requests
1 parent 9432399 commit eb745e6

File tree

19 files changed

+286
-122
lines changed

19 files changed

+286
-122
lines changed
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
4-
<dict/>
4+
<dict>
5+
<key>UID2EnvironmentEUID</key>
6+
<false/>
7+
</dict>
58
</plist>

Development/UID2SDKDevelopmentApp/UID2SDKDevelopmentApp/Localizable.strings

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
"common.nil" = "Nil";
1010

11-
"root.navigation.title" = "UID2 SDK Dev App";
11+
"root.uid2.navigation.title" = "UID2 SDK Dev App";
12+
"root.euid.navigation.title" = "EUID SDK Dev App";
1213
"root.title.identitypackage" = "Current Identity";
1314
"root.label.error" = "Error Occurred";
1415
"root.button.reset" = "Reset";

Development/UID2SDKDevelopmentApp/UID2SDKDevelopmentApp/Networking/AppUID2Client.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ internal final class AppUID2Client: Sendable {
173173
func decryptResponse(_ b64Secret: String, _ responseData: Data, _ isRefresh: Bool = false) -> Data? {
174174

175175
// Confirm that responseData is Base64
176+
// swiftlint:disable:next non_optional_string_data_conversion
176177
guard let base64String = String(data: responseData, encoding: .utf8),
177178
let decodedData = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) else {
178179
return responseData
@@ -211,6 +212,7 @@ internal final class AppUID2Client: Sendable {
211212
payload = decryptedData.subdata(in: 16..<decryptedData.count)
212213
}
213214

215+
// swiftlint:disable:next non_optional_string_data_conversion
214216
guard let _ = String(data: payload, encoding: .utf8) else {
215217
return nil
216218
}

Development/UID2SDKDevelopmentApp/UID2SDKDevelopmentApp/RootView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ struct RootView: View {
2525
var body: some View {
2626

2727
VStack {
28-
Text("root.navigation.title")
28+
Text(viewModel.isEUID ? "root.euid.navigation.title" : "root.uid2.navigation.title")
2929
.font(Font.system(size: 28, weight: .bold))
3030
HStack {
3131
TextField("Email Address", text: $email)
@@ -85,6 +85,7 @@ extension TokenGenerationError: LocalizedError {
8585
if let message,
8686
let jsonObject = try? JSONSerialization.jsonObject(with: Data(message.utf8)),
8787
let jsonString = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted) {
88+
// swiftlint:disable:next non_optional_string_data_conversion
8889
formattedMessage = String(data: jsonString, encoding: .utf8)
8990
} else {
9091
formattedMessage = message

Development/UID2SDKDevelopmentApp/UID2SDKDevelopmentApp/RootViewModel.swift

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,41 @@
77

88
import Combine
99
import Foundation
10+
import OSLog
1011
import SwiftUI
1112
import UID2
1213

14+
extension RootViewModel {
15+
struct Configuration {
16+
let subscriptionID: String
17+
let appName: String
18+
let serverPublicKeyString: String
19+
20+
static func uid2() -> Self {
21+
self.init(
22+
subscriptionID: "toPh8vgJgt",
23+
appName: Bundle.main.bundleIdentifier!,
24+
// swiftlint:disable:next line_length
25+
serverPublicKeyString: "UID2-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKAbPfOz7u25g1fL6riU7p2eeqhjmpALPeYoyjvZmZ1xM2NM8UeOmDZmCIBnKyRZ97pz5bMCjrs38WM22O7LJuw=="
26+
)
27+
}
28+
29+
static func euid() -> Self {
30+
self.init(
31+
subscriptionID: "w6yPQzN4dA",
32+
appName: Bundle.main.bundleIdentifier!,
33+
// swiftlint:disable:next line_length
34+
serverPublicKeyString: "EUID-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEH/k7HYGuWhjhCo8nXgj/ypClo5kek7uRKvzCGwj04Y1eXOWmHDOLAQVCPquZdfVVezIpABNAl9zvsSEC7g+ZGg=="
35+
)
36+
}
37+
}
38+
}
39+
1340
@MainActor
14-
class RootViewModel: ObservableObject {
15-
41+
final class RootViewModel: ObservableObject {
42+
43+
let isEUID: Bool
44+
1645
@Published private(set) var uid2Identity: UID2Identity? {
1746
didSet {
1847
error = nil
@@ -25,19 +54,33 @@ class RootViewModel: ObservableObject {
2554

2655
/// `UID2Settings` must be configured prior to accessing the `UID2Manager` instance.
2756
/// Configuring them here makes it less likely that an access occurs before configuration.
28-
private let manager: UID2Manager = {
57+
private let manager: UID2Manager
58+
59+
private let configuration: Configuration
60+
61+
private let log = OSLog(subsystem: "com.uid2.UID2SDKDevelopmentApp", category: "RootViewModel")
62+
63+
init() {
64+
isEUID = Bundle.main.object(forInfoDictionaryKey: "UID2EnvironmentEUID") as? Bool ?? false
65+
2966
UID2Settings.shared.isLoggingEnabled = true
3067
// Only the development app should use the integration environment.
3168
// If you have copied the dev app for testing, you probably want to use the default
3269
// environment, which is production.
3370
if Bundle.main.bundleIdentifier == "com.uid2.UID2SDKDevelopmentApp" {
34-
UID2Settings.shared.environment = .custom(url: URL(string: "https://operator-integ.uidapi.com")!)
71+
UID2Settings.shared.euidEnvironment = .custom(url: URL(string: "https://integ.euid.eu/v2")!)
72+
UID2Settings.shared.uid2Environment = .custom(url: URL(string: "https://operator-integ.uidapi.com")!)
3573
}
3674

37-
return UID2Manager.shared
38-
}()
39-
40-
init() {
75+
if isEUID {
76+
os_log("Configured for EUID", log: log, type: .info)
77+
configuration = .euid()
78+
manager = EUIDManager.shared
79+
} else {
80+
os_log("Configured for UID2", log: log, type: .info)
81+
configuration = .uid2()
82+
manager = UID2Manager.shared
83+
}
4184
Task {
4285
for await state in await manager.stateValues() {
4386
self.uid2Identity = state?.identity
@@ -130,17 +173,13 @@ class RootViewModel: ObservableObject {
130173
}
131174

132175
func clientSideGenerate(identity: IdentityType) {
133-
let subscriptionID = "toPh8vgJgt"
134-
// swiftlint:disable:next line_length
135-
let serverPublicKeyString = "UID2-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKAbPfOz7u25g1fL6riU7p2eeqhjmpALPeYoyjvZmZ1xM2NM8UeOmDZmCIBnKyRZ97pz5bMCjrs38WM22O7LJuw=="
136-
137176
Task<Void, Never> {
138177
do {
139178
try await manager.generateIdentity(
140179
identity,
141-
subscriptionID: subscriptionID,
142-
serverPublicKey: serverPublicKeyString,
143-
appName: Bundle.main.bundleIdentifier!
180+
subscriptionID: configuration.subscriptionID,
181+
serverPublicKey: configuration.serverPublicKeyString,
182+
appName: configuration.appName
144183
)
145184
} catch {
146185
self.error = error

Sources/UID2/EUIDManager.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// EUIDManager.swift
3+
//
4+
5+
import Foundation
6+
7+
public final class EUIDManager {
8+
9+
/// Singleton access point for EUID Manager
10+
/// Returns a manager configured for use with EUID.
11+
public static let shared: UID2Manager = {
12+
UID2Manager(
13+
environment: Environment(UID2Settings.shared.euidEnvironment),
14+
account: .euid
15+
)
16+
}()
17+
}

Sources/UID2/Environment.swift

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,74 @@
77

88
import Foundation
99

10-
/// For more information, see https://unifiedid.com/docs/getting-started/gs-environments
11-
public struct Environment: Hashable, Sendable {
10+
/// Internal Environment representation
11+
struct Environment: Hashable, Sendable {
1212

1313
/// API base URL
14-
var endpoint: URL
15-
16-
/// Equivalent to `ohio`
17-
public static let production = ohio
18-
19-
/// AWS US East (Ohio)
20-
public static let ohio = Self(endpoint: URL(string: "https://prod.uidapi.com")!)
21-
/// AWS US West (Oregon)
22-
public static let oregon = Self(endpoint: URL(string: "https://usw.prod.uidapi.com")!)
23-
/// AWS Asia Pacific (Singapore)
24-
public static let singapore = Self(endpoint: URL(string: "https://sg.prod.uidapi.com")!)
25-
/// AWS Asia Pacific (Sydney)
26-
public static let sydney = Self(endpoint: URL(string: "https://au.prod.uidapi.com")!)
27-
/// AWS Asia Pacific (Tokyo)
28-
public static let tokyo = Self(endpoint: URL(string: "https://jp.prod.uidapi.com")!)
29-
30-
/// A custom endpoint
31-
public static func custom(url: URL) -> Self {
32-
Self(endpoint: url)
14+
let endpoint: URL
15+
let isProduction: Bool
16+
}
17+
18+
extension Environment {
19+
init(_ environment: UID2.Environment) {
20+
endpoint = environment.endpoint
21+
isProduction = (environment == .production)
22+
}
23+
24+
init(_ environment: EUID.Environment) {
25+
endpoint = environment.endpoint
26+
isProduction = (environment == .production)
27+
}
28+
}
29+
30+
// Namespaces
31+
public enum EUID {}
32+
public enum UID2 {}
33+
34+
extension UID2 {
35+
/// For more information, see https://unifiedid.com/docs/getting-started/gs-environments
36+
public struct Environment: Hashable, Sendable {
37+
38+
/// API base URL
39+
var endpoint: URL
40+
41+
/// Equivalent to `ohio`
42+
public static let production = ohio
43+
44+
/// AWS US East (Ohio)
45+
public static let ohio = Self(endpoint: URL(string: "https://prod.uidapi.com")!)
46+
/// AWS US West (Oregon)
47+
public static let oregon = Self(endpoint: URL(string: "https://usw.prod.uidapi.com")!)
48+
/// AWS Asia Pacific (Singapore)
49+
public static let singapore = Self(endpoint: URL(string: "https://sg.prod.uidapi.com")!)
50+
/// AWS Asia Pacific (Sydney)
51+
public static let sydney = Self(endpoint: URL(string: "https://au.prod.uidapi.com")!)
52+
/// AWS Asia Pacific (Tokyo)
53+
public static let tokyo = Self(endpoint: URL(string: "https://jp.prod.uidapi.com")!)
54+
55+
/// A custom endpoint
56+
public static func custom(url: URL) -> Self {
57+
Self(endpoint: url)
58+
}
59+
}
60+
}
61+
62+
extension EUID {
63+
/// See https://euid.eu/docs/getting-started/gs-environments
64+
public struct Environment: Hashable, Sendable {
65+
66+
/// API base URL
67+
var endpoint: URL
68+
69+
/// Equivalent to `london`
70+
public static let production = london
71+
72+
/// AWS EU West 2 (London)
73+
public static let london = Self(endpoint: URL(string: "https://prod.euid.eu/v2")!)
74+
75+
/// A custom endpoint
76+
public static func custom(url: URL) -> Self {
77+
Self(endpoint: url)
78+
}
3379
}
3480
}

Sources/UID2/KeychainManager.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import Foundation
66
import Security
77

88
extension Storage {
9-
static func keychainStorage() -> Storage {
10-
let storage = KeychainManager()
9+
static func keychainStorage(account: Account) -> Storage {
10+
let storage = KeychainManager(account: account)
1111
return .init(
1212
loadIdentity: { await storage.loadIdentity() },
1313
saveIdentity: { await storage.saveIdentity($0) },
@@ -16,13 +16,23 @@ extension Storage {
1616
}
1717
}
1818

19+
/// These RawValue are used as persistence keys and must not be renamed
20+
enum Account: String {
21+
case uid2 = "uid2" // swiftlint:disable:this redundant_string_enum_value
22+
case euid = "euid" // swiftlint:disable:this redundant_string_enum_value
23+
}
24+
1925
/// Securely manages data in the Keychain
2026
actor KeychainManager {
2127

22-
private let attrAccount = "uid2"
28+
private let attrAccount: Account
2329

2430
private static let attrService = "auth-state"
2531

32+
init(account: Account = .uid2) {
33+
attrAccount = account
34+
}
35+
2636
func loadIdentity() -> IdentityPackage? {
2737
let query = query(with: [
2838
String(kSecReturnData): true
@@ -77,7 +87,7 @@ actor KeychainManager {
7787
private func query(with queryElements: [String: Any]) -> CFDictionary {
7888
let commonElements = [
7989
String(kSecClass): kSecClassGenericPassword,
80-
String(kSecAttrAccount): attrAccount,
90+
String(kSecAttrAccount): attrAccount.rawValue,
8191
String(kSecAttrService): Self.attrService
8292
] as [String: Any]
8393

Sources/UID2/Networking/DataEnvelope.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ internal enum DataEnvelope {
4444
extension Data {
4545
/// A convenience initializer for converting from a Data representation of a base64 encoded string to its decoded Data.
4646
init?(base64EncodedData: Data, options: Data.Base64DecodingOptions = []) {
47+
// https://github.com/realm/SwiftLint/issues/5263#issuecomment-2115182747
48+
// swiftlint:disable:next non_optional_string_data_conversion
4749
guard let base64String = String(data: base64EncodedData, encoding: .utf8) else {
4850
return nil
4951
}

Sources/UID2/UID2Client.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal final class UID2Client: Sendable {
2525
init(
2626
sdkVersion: String,
2727
isLoggingEnabled: Bool = false,
28-
environment: Environment = .production,
28+
environment: Environment,
2929
session: NetworkSession = URLSession.shared,
3030
cryptoUtil: CryptoUtil = .liveValue
3131
) {
@@ -120,9 +120,11 @@ internal final class UID2Client: Sendable {
120120
let decoder = JSONDecoder.apiDecoder()
121121
guard response.statusCode == 200 else {
122122
let statusCode = response.statusCode
123+
// https://github.com/realm/SwiftLint/issues/5263#issuecomment-2115182747
124+
// swiftlint:disable:next non_optional_string_data_conversion
123125
let responseText = String(data: data, encoding: .utf8) ?? "<none>"
124126
os_log("Request failure (%d) %@", log: log, type: .error, statusCode, responseText)
125-
if environment != .production {
127+
if !environment.isProduction {
126128
os_log("Failed request is using non-production API endpoint %@, is this intentional?", log: log, type: .error, baseURL.description)
127129
}
128130
throw TokenGenerationError.requestFailure(

0 commit comments

Comments
 (0)