-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathEdgeAPI.swift
More file actions
244 lines (203 loc) · 8.22 KB
/
Copy pathEdgeAPI.swift
File metadata and controls
244 lines (203 loc) · 8.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
//
// EdgeAPI.swift
// OptableSDK
//
// Copyright © 2026 Optable Technologies, Inc. All rights reserved.
//
import Foundation
import WebKit
// MARK: - EdgeAPI
/**
Real Time API
For more info check:
[](https://docs.optable.co/optable-documentation/guides/real-time-api-integrations-guide)
*/
final class EdgeAPI {
private let kPassportHeader: String = "X-Optable-Visitor"
var storage: LocalStorage
var config: OptableConfig
var userAgent: String?
private lazy var jsonEncoder = JSONEncoder()
init(_ config: OptableConfig) {
self.config = config
self.storage = LocalStorage(config)
if config.customUserAgent == nil {
self.resolveUserAgent { realUserAgent in
self.userAgent = realUserAgent
}
} else {
self.userAgent = config.customUserAgent
}
}
// MARK: Endpoints
func identify(ids: [OptableIdentifier]) throws -> URLRequest? {
guard let url = buildEdgeAPIURL(endpoint: "identify") else { return nil }
let jsonData = try jsonEncoder.encode(ids)
let request = try buildRequest(.POST, url: url, headers: resolveHeaders(), data: jsonData)
return request
}
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]
if let id {
payload["id"] = id
}
if let neighbors, neighbors.isEmpty == false {
payload["neighbors"] = neighbors
}
let request = try buildRequest(.POST, url: url, headers: resolveHeaders(), obj: payload)
return request
}
func targeting(ids: [OptableIdentifier]) throws -> URLRequest? {
guard var url = buildEdgeAPIURL(endpoint: "targeting") else { return nil }
let queryItems = ids
.compactMap({ $0.extendedIdentifier })
.compactMap({ URLQueryItem(name: "id", value: $0) })
url.compatAppend(queryItems: queryItems)
let request = try buildRequest(.GET, url: url, headers: resolveHeaders())
return request
}
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
}
}
// MARK: - Dispatch
extension EdgeAPI {
func dispatch(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
return URLSession.shared.dataTask(with: request) { data, response, error in
guard let res = response as? HTTPURLResponse, error == nil else {
completionHandler(data, response, error)
return
}
guard 200 ..< 300 ~= res.statusCode else {
completionHandler(data, response, error)
return
}
if #available(iOS 13.0, *) {
if let passport = res.value(forHTTPHeaderField: self.kPassportHeader) {
self.storage.setPassport(passport)
}
} else {
// In older versions of iOS, we have to resort searching through headers via res.allHeaderFields
// Unlike res.value(forHTTPHeaderField:...) which was introduced in iOS 13.0, allHeaderFields is
// case-sensitive, so we need to take special care to perform a case-INsensitive search:
for (key, value) in res.allHeaderFields {
if let header = key as? String {
let result: ComparisonResult = header.compare(self.kPassportHeader, options: NSString.CompareOptions.caseInsensitive)
if result == .orderedSame {
if let pp = value as? String {
self.storage.setPassport(pp)
break
}
}
}
}
}
completionHandler(data, response, error)
}
}
}
// MARK: - Private
extension EdgeAPI {
private func resolveUserAgent(callback: @escaping (_ useragent: String) -> Void) {
var wkUserAgent = ""
let myGroup = DispatchGroup()
let window = UIApplication.shared.keyWindow
let webView = WKWebView(frame: UIScreen.main.bounds)
webView.isHidden = true
window?.addSubview(webView)
myGroup.enter()
webView.loadHTMLString("<html></html>", baseURL: nil)
webView.evaluateJavaScript("navigator.userAgent", completionHandler: { (userAgent: Any?, error: Error?) in
if let userAgent = userAgent as? String {
wkUserAgent = userAgent
}
webView.stopLoading()
webView.removeFromSuperview()
myGroup.leave()
})
myGroup.notify(queue: .main) {
callback(wkUserAgent)
}
}
func resolveHeaders() -> HTTPHeaders {
var headers = HTTPHeaders()
headers[.accept] = "application/json"
headers[.contentType] = "application/json"
if let userAgent {
headers[.userAgent] = userAgent
}
if let apiKey = config.apiKey {
headers[.authorization] = "Bearer \(apiKey)"
}
if let passport: String = storage.getPassport() {
headers[kPassportHeader] = passport
}
return headers
}
private func buildRequest(_ method: HTTPMethod, url: URL, headers: HTTPHeaders, obj: Any? = nil) throws -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
if let obj = obj {
let reqBodyJSON = try JSONSerialization.data(withJSONObject: obj, options: [])
request.httpBody = reqBodyJSON
}
for (key, value) in headers.asDict {
request.addValue(value, forHTTPHeaderField: key)
}
return request
}
private func buildRequest(_ method: HTTPMethod, url: URL, headers: HTTPHeaders, data: Data? = nil) throws -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
if let data {
request.httpBody = data
}
for (key, value) in headers.asDict {
request.addValue(value, forHTTPHeaderField: key)
}
return request
}
func buildEdgeAPIURL(endpoint: String) -> URL? {
var components = URLComponents()
components.scheme = config.insecure ? "http" : "https"
components.host = config.host
components.path = "/\(config.path)/\(endpoint)"
components.queryItems = [
.init(name: "t", value: config.tenant),
.init(name: "o", value: config.originSlug),
.init(name: "osdk", value: OptableSDK.version),
]
if let reg = config.reg {
components.queryItems?.append(.init(name: "reg", value: reg))
}
if let gdprConsent = config.gdprConsent, let gdpr = config.gdpr?.boolValue {
components.queryItems?.append(contentsOf: [
.init(name: "gdpr_consent", value: gdprConsent),
.init(name: "gdpr", value: "\(gdpr ? 1 : 0)"),
])
} else if let globalGDPRConsent = IABConsent.gdprTC, let globalGDPR = IABConsent.gdprApplies {
components.queryItems?.append(contentsOf: [
.init(name: "gdpr_consent", value: globalGDPRConsent),
.init(name: "gdpr", value: "\(globalGDPR ? 1 : 0)"),
])
}
if let gpp = config.gpp {
components.queryItems?.append(
.init(name: "gpp", value: gpp)
)
} else if let globalGPP = IABConsent.gppTC {
components.queryItems?.append(
.init(name: "gpp", value: globalGPP)
)
}
if let gppSid = config.gppSid {
components.queryItems?.append(
.init(name: "gpp_sid", value: gppSid)
)
}
return components.url
}
}