This repository was archived by the owner on Aug 25, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathBLOCKv.swift
More file actions
378 lines (304 loc) · 13.5 KB
/
BLOCKv.swift
File metadata and controls
378 lines (304 loc) · 13.5 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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
//
// BlockV AG. Copyright (c) 2018, all rights reserved.
//
// Licensed under the BlockV SDK License (the "License"); you may not use this file or
// the BlockV SDK except in compliance with the License accompanying it. Unless
// required by applicable law or agreed to in writing, the BlockV SDK distributed under
// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
// ANY KIND, either express or implied. See the License for the specific language
// governing permissions and limitations under the License.
//
import Foundation
import Alamofire
import JWTDecode
import Nuke
/*
Goal:
BLOCKv should be invariant over App ID and Environment. In other words, the properties should not
change, once set. Possibly targets for each environemnt?
*/
/// Primary interface into the the BLOCKv SDK.
public final class BLOCKv {
// MARK: - Properties
/// The App ID to be passed to the BLOCKv platform.
///
/// Must be set once by the host app.
internal fileprivate(set) static var appID: String? {
// willSet is only called outside of the initialisation context, i.e.
// setting the appID after its init will cause a fatal error.
willSet {
if appID != nil {
assertionFailure("The App ID may be set only once.")
}
}
}
//TODO: Detect an environment switch, e.g. dev to prod, reset the client.
/// The BLOCKv platform environment to use.
///
/// Must be set by the host app.
internal fileprivate(set) static var environment: BVEnvironment? {
willSet {
if environment != nil { reset() }
}
didSet { printBV(info: "Environment updated - \(environment!)") }
}
// MARK: - Configuration
/// Configures the SDK with your issued app id.
///
/// Note, as a viewer, `configure` should be the first method call you make on the BLOCKv SDK.
/// Typically, you would call `configure` in `application(_:didFinishLaunchingWithOptions:)`
///
/// This method must be called ONLY once.
public static func configure(appID: String) {
self.appID = appID
// - CONFIGURE ENVIRONMENT
// only modify if not set
if environment == nil {
/*
The presense of the ENVIRONMENT_MAPPING user defined plist key allows the SDK to use pre-mapped
environments. This is only used internally for the BLOCKv apps. 3rd party API consumers must always use
the production environment.
*/
// check if the plist contains a user defined key (internal only)
if let environmentString = Bundle.main.infoDictionary?["ENVIRONMENT_MAPPING"] as? String,
let mappedEnvironment = BVEnvironment(rawValue: environmentString) {
#if DEBUG
// environment for experimentation (safe to modify)
self.environment = .production
#else
// pre-mapped environment (do not modify)
self.environment = mappedEnvironment
#endif
} else {
// 3rd party API consumers must always point to production.
self.environment = .production
}
}
// NOTE: Since `configure` is called only once in the app's lifecycle. We do not
// need to worry about multiple registrations.
NotificationCenter.default.addObserver(BLOCKv.self,
selector: #selector(handleUserAuthorisationRequired),
name: Notification.Name.BVInternal.UserAuthorizationRequried,
object: nil)
// configure in-memory cache (store processed images ready for display)
ImageCache.shared.costLimit = ImageCache.defaultCostLimit()
// configure http cache (store unprocessed image data at the http level)
DataLoader.sharedUrlCache.memoryCapacity = 80 * 1024 * 1024 // 80 MB
DataLoader.sharedUrlCache.diskCapacity = 180 // 180 MB
// handle session launch
if self.isLoggedIn {
self.onSessionLaunch()
}
}
// MARK: - Client
// FIXME: Should this be nil on logout?
// FIXME: This MUST become a singleton (since only a single instance should ever exist).
private static let oauthHandler = OAuth2Handler(appID: BLOCKv.appID!,
baseURLString: BLOCKv.environment!.apiServerURLString,
refreshToken: CredentialStore.refreshToken?.token ?? "")
/// Computes the configuration object needed to initialise clients and sockets.
fileprivate static var clientConfiguration: Client.Configuration {
// ensure host app has set an app id
let warning = """
Please call 'BLOCKv.configure(appID:)' with your issued app ID before making network
requests.
"""
precondition(BLOCKv.appID != nil, warning)
// return the configuration (inexpensive object)
return Client.Configuration(baseURLString: BLOCKv.environment!.apiServerURLString,
appID: BLOCKv.appID!)
}
/// Backing networking client instance.
fileprivate static var _client: Client?
/// BLOCKv networking client.
///
/// The networking client must support a platform environment change after app launch.
///
/// This requirement is met by using a computed property that dynamically initialises a
/// new client if the instance variable `_client` has been set to `nil`.
///
/// The affords the caller the ability to set the platform environment and be sure to
/// receive a new networking client instance.
static var client: Client {
// check if a new instance must be initialized
if _client == nil {
// init a new instance
_client = Client(config: BLOCKv.clientConfiguration,
oauthHandler: self.oauthHandler)
return _client!
} else {
// return the backing instance
return _client!
}
}
// MARK: - Web socket
/*
Client and Socket are mutually exclusive. That is, one can be created with out the other.
Both relay on the ability to retrieve an access token. This is provided by a shared instance
of OAuth2Handler.
Even though client and socket are independent, the socket is dependent on the user having an
authenticated session.
The socket must handle the case where the user is unauthenticated. Particularly around logout.
*/
/// Backing Web socket instance.
///
/// Must be torn down when the user logs out.
///
/// The Web socket is independent of the `client`. However, it is bound to
/// the user being authenticaated.
fileprivate static var _socket: WebSocketManager?
//TODO: What if this is accessed before the client is accessed?
//TODO: What if the viewer subscribes to an event before auth (login/reg) has occured?
public static var socket: WebSocketManager {
if _socket == nil {
_socket = WebSocketManager(baseURLString: self.environment!.webSocketURLString,
appID: self.appID!,
oauthHandler: self.oauthHandler)
return _socket!
} else {
return _socket!
}
}
// MARK: - Lifecycle
/// Call to reset the SDK.
internal static func reset() {
// remove all credentials
CredentialStore.clear()
// nil out client
self._client = nil
// disconnect and nil out socekt
self._socket?.disconnect()
self._socket = nil
// clear data pool
DataPool.clear()
printBV(info: "Reseting SDK")
}
// - Public Lifecycle
/// Boolean indicating whether a user is logged in. `true` if logged in. `false` otherwise.
public static var isLoggedIn: Bool {
// ensure a token is present
guard let refreshToken = CredentialStore.refreshToken?.token else { return false }
// ensure a valid jwt
guard let refreshJWT = try? decode(jwt: refreshToken) else { return false }
// ensure still valid
return !refreshJWT.expired
}
@available(*, deprecated, message: "Unsupported feature of the SDK and may be removed in the future.")
/// Retrieves a refreshed access token.
///
/// - Important:
/// This function should only be called if you have a well defined reason for obtaining an
/// access token.
///
/// - Parameter completion: The closure to call once an access token has been obtained
/// form the BLOCKv platform.
public static func getAccessToken(completion: @escaping (_ success: Bool, _ accessToken: String?) -> Void) {
BLOCKv.client.getAccessToken(completion: completion)
}
/// Called when the networking client detects the user is unauthenticated.
///
/// This method perfroms a clean up operation before notifying the viewer that the SDK requires
/// user authentication.
///
/// - important: This method may be called multiple times. For example, consider the case where
/// multiple requests fail due to the refresh token being invalid.
@objc
private static func handleUserAuthorisationRequired() {
printBV(info: "Authorization - User is unauthorized.")
// only notify the viewer if the user is currently authorized
if isLoggedIn {
// perform interal clean up
reset()
// call the closure stored in `onLogout`
onLogout?()
}
}
/// Called when the user authenticates (logs in).
///
/// - important:
/// This method is *not* called when the access token refreshes.
static internal func onLogin() {
// stand up the session
self.onSessionLaunch()
}
/// Holds a closure to call on logout
public static var onLogout: (() -> Void)?
/// This function is called everytime a user session is launched.
///
/// A 'session launch' means the user has logged in (received a new refresh token), or the app has been cold
/// launched with an existing *valid* refresh token.
///
/// - note:
/// This is slightly broader than 'log in' since it includes the lifecycle of the app. This function is responsible
/// for creating objects which are depenedent on a user session, e.g. data pool.
///
/// Its compainion `onSessionTerminated` is `onLogout` since there is no app event signalling app termination.
///
/// Triggered by:
/// - User authentication
/// - App launch & user is authenticated
static private func onSessionLaunch() {
guard let refreshToken = CredentialStore.refreshToken?.token else {
fatalError("Invlalid session")
}
guard let claim = try? decode(jwt: refreshToken).claim(name: "user_id"), let userId = claim.string else {
fatalError("Invalid cliam")
}
// standup the client & socket
_ = client
_ = socket.connect()
// standup data pool
DataPool.sessionInfo = ["userID": userId]
}
// MARK: - Resources
enum URLEncodingError: Error {
case missingAssetProviders
}
/// Encodes the URL with the with the available asset providers.
///
/// - note: Not all URLs require asset provider encoding.
///
/// If the SDK does not have any asset provider credentials the method will throw.
public static func encodeURL(_ url: URL) throws -> URL {
let assetProviders = CredentialStore.assetProviders
if assetProviders.isEmpty { throw URLEncodingError.missingAssetProviders }
let provider = assetProviders.first(where: { $0.isProviderForURL(url) })
return provider?.encodedURL(url) ?? url
}
/// Closure that encodes a given url using a set of asset providers.
///
/// If none of the asset providers are able to perform encoding, the original URL is returned.
internal static let blockvURLEncoder: URLEncoder = { (url, assetProviders) in
let provider = assetProviders.first(where: { $0.isProviderForURL(url) })
return provider?.encodedURL(url) ?? url
}
// MARK: - Init
/// BLOCKv follows the static pattern. Instance creation is not allowed.
fileprivate init() {}
}
// MARK: - Print Helpers
func printBV(info string: String) {
print("\nBV SDK > \(string)")
}
func printBV(error string: String) {
print("\nBV SDK >>> Error: \(string)")
}
extension BLOCKv {
public enum Debug {
//// Returns the cache size of the face data resource disk caches.
public static var faceDataResourceCacheSize: UInt64? {
return try? FileManager.default.allocatedSizeOfDirectory(at: DataDownloader.recommendedCacheDirectory)
}
/// Returns the cache size of all data pool region disk caches.
public static var regionCacheSize: UInt64? {
return try? FileManager.default.allocatedSizeOfDirectory(at: Region.recommendedCacheDirectory)
}
/// Clears all disk caches.
public static func clearCache() {
ImageCache.shared.removeAll()
DataLoader.sharedUrlCache.removeAllCachedResponses()
try? FileManager.default.removeItem(at: DataDownloader.recommendedCacheDirectory)
try? FileManager.default.removeItem(at: Region.recommendedCacheDirectory)
}
}
}