vibes-react-native 5.0.0 → 5.0.1-rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/ios/Vibes.swift +2 -3
  2. package/ios/VibesClient.swift +3 -4
  3. package/ios/VibesPluginLogger.swift +2 -5
  4. package/package.json +3 -2
  5. package/vendor/VibesPush/LICENSE +13 -0
  6. package/vendor/VibesPush/Sources/APIDefinition.swift +502 -0
  7. package/vendor/VibesPush/Sources/Actions.swift +24 -0
  8. package/vendor/VibesPush/Sources/AppObserver.swift +137 -0
  9. package/vendor/VibesPush/Sources/AuthTokenBehavior.swift +17 -0
  10. package/vendor/VibesPush/Sources/CombinedLogger.swift +53 -0
  11. package/vendor/VibesPush/Sources/ConsoleLogger.swift +97 -0
  12. package/vendor/VibesPush/Sources/Credential.swift +41 -0
  13. package/vendor/VibesPush/Sources/CredentialManager.swift +24 -0
  14. package/vendor/VibesPush/Sources/Date+iso8601.swift +34 -0
  15. package/vendor/VibesPush/Sources/Date+logPrefix.swift +20 -0
  16. package/vendor/VibesPush/Sources/DeviceToken.swift +15 -0
  17. package/vendor/VibesPush/Sources/DeviceType.swift +143 -0
  18. package/vendor/VibesPush/Sources/Dictionary+merged.swift +11 -0
  19. package/vendor/VibesPush/Sources/Event.swift +83 -0
  20. package/vendor/VibesPush/Sources/EventCollection.swift +36 -0
  21. package/vendor/VibesPush/Sources/EventTracker.swift +39 -0
  22. package/vendor/VibesPush/Sources/FoundationExtensions.swift +16 -0
  23. package/vendor/VibesPush/Sources/GitTag.swift +27 -0
  24. package/vendor/VibesPush/Sources/HTTP/HTTPBehavior.swift +19 -0
  25. package/vendor/VibesPush/Sources/HTTP/HTTPMethod.swift +20 -0
  26. package/vendor/VibesPush/Sources/HTTP/HTTPResource.swift +69 -0
  27. package/vendor/VibesPush/Sources/HTTP/HTTPResourceClient.swift +190 -0
  28. package/vendor/VibesPush/Sources/HTTP/JSONParser.swift +91 -0
  29. package/vendor/VibesPush/Sources/HTTP/Operations/AssociatePersonOperation.swift +56 -0
  30. package/vendor/VibesPush/Sources/HTTP/Operations/BaseOperation.swift +153 -0
  31. package/vendor/VibesPush/Sources/HTTP/Operations/EventOperation.swift +73 -0
  32. package/vendor/VibesPush/Sources/HTTP/Operations/GetGitTagsOperation.swift +61 -0
  33. package/vendor/VibesPush/Sources/HTTP/Operations/GetInboxMessageOperation.swift +59 -0
  34. package/vendor/VibesPush/Sources/HTTP/Operations/GetInboxMessagesOperation.swift +57 -0
  35. package/vendor/VibesPush/Sources/HTTP/Operations/GetPersonOperation.swift +53 -0
  36. package/vendor/VibesPush/Sources/HTTP/Operations/GetVibesAppInfoOperation.swift +45 -0
  37. package/vendor/VibesPush/Sources/HTTP/Operations/MigrationCallbackOperation.swift +45 -0
  38. package/vendor/VibesPush/Sources/HTTP/Operations/RegisterDeviceOperation.swift +66 -0
  39. package/vendor/VibesPush/Sources/HTTP/Operations/RegisterPushOperation.swift +73 -0
  40. package/vendor/VibesPush/Sources/HTTP/Operations/TrackProductActionOperation.swift +75 -0
  41. package/vendor/VibesPush/Sources/HTTP/Operations/TrackPurchaseActionOperation.swift +81 -0
  42. package/vendor/VibesPush/Sources/HTTP/Operations/UnregisterDeviceOperation.swift +64 -0
  43. package/vendor/VibesPush/Sources/HTTP/Operations/UnregisterPushOperation.swift +72 -0
  44. package/vendor/VibesPush/Sources/HTTP/Operations/UpdateDeviceOperationPatch.swift +57 -0
  45. package/vendor/VibesPush/Sources/HTTP/Operations/UpdateDeviceOperationPut.swift +59 -0
  46. package/vendor/VibesPush/Sources/HTTP/Operations/UpdateMessageOperation.swift +50 -0
  47. package/vendor/VibesPush/Sources/HTTP/Operations/VibesQueueOperation.swift +76 -0
  48. package/vendor/VibesPush/Sources/HTTP/ResourceResult.swift +38 -0
  49. package/vendor/VibesPush/Sources/HTTPURLResponse+cURL.swift +16 -0
  50. package/vendor/VibesPush/Sources/InboxMessage.swift +219 -0
  51. package/vendor/VibesPush/Sources/Info.plist +26 -0
  52. package/vendor/VibesPush/Sources/InputStream+readToString.swift +19 -0
  53. package/vendor/VibesPush/Sources/KeychainStorage.swift +64 -0
  54. package/vendor/VibesPush/Sources/LocalObject.swift +25 -0
  55. package/vendor/VibesPush/Sources/LocalStorageKeys.swift +19 -0
  56. package/vendor/VibesPush/Sources/LocalStorageType.swift +47 -0
  57. package/vendor/VibesPush/Sources/LocalValue.swift +67 -0
  58. package/vendor/VibesPush/Sources/PersistentEventStorage.swift +81 -0
  59. package/vendor/VibesPush/Sources/Person.swift +98 -0
  60. package/vendor/VibesPush/Sources/Product.swift +103 -0
  61. package/vendor/VibesPush/Sources/Purchase.swift +108 -0
  62. package/vendor/VibesPush/Sources/TrackedEventType.swift +48 -0
  63. package/vendor/VibesPush/Sources/URLRequest+cURL.swift +36 -0
  64. package/vendor/VibesPush/Sources/UserDefaultsStorage.swift +32 -0
  65. package/vendor/VibesPush/Sources/Vibes.swift +1069 -0
  66. package/vendor/VibesPush/Sources/VibesAPI.swift +81 -0
  67. package/vendor/VibesPush/Sources/VibesAPIDelegate.swift +41 -0
  68. package/vendor/VibesPush/Sources/VibesAppInfo.swift +77 -0
  69. package/vendor/VibesPush/Sources/VibesConfiguration.swift +108 -0
  70. package/vendor/VibesPush/Sources/VibesLogger.swift +65 -0
  71. package/vendor/VibesPush/Sources/VibesResult.swift +78 -0
  72. package/vibes-react-native.podspec +6 -2
@@ -0,0 +1,1069 @@
1
+ import UIKit
2
+ // swiftlint:disable line_length
3
+
4
+ /// A closure used as a completion handler for interacting with the Vibes
5
+ /// object.
6
+ public typealias VibesCompletion<A> = (VibesResult<A>) -> Void
7
+
8
+ /// Type Alias for Get Inbox Message callback
9
+ public typealias GetInboxMessageCallback = (_ message: InboxMessage?, _ error: Error?) -> Void
10
+
11
+ /// Type Alias for Get Git Tags callback
12
+ public typealias GetGitTagsCallback = (_ tags: [GitTag], _ error: Error?) -> Void
13
+
14
+ /// Type Alias for Get Inbox Messages callback
15
+ public typealias GetInboxMessagesCallback = (_ messages: [InboxMessage], _ error: Error?) -> Void
16
+
17
+ /// Type Alias for update Inbox Message callback
18
+ public typealias UpdateMessageCallback = (_ messages: InboxMessage?, _ error: Error?) -> Void
19
+
20
+ /// Type Alias for Get Vibes Person callback
21
+ public typealias GetPersonCallback = (_ person: Person?, _ error: Error?) -> Void
22
+
23
+ /// Type Alias for Getting App info callback
24
+ public typealias GetVibesAppInfoCallback = (_ status: VibesAppInfo?, _ error: Error?) -> Void
25
+
26
+ /// Type Alias for Track Product Action callback
27
+ public typealias TrackProductActionCallback = (_ error: Error?) -> Void
28
+
29
+ /// Type Alias for Track Purchase Action callback
30
+ public typealias TrackPurchaseActionCallback = (_ error: Error?) -> Void
31
+
32
+ /// Type of storage to store credential
33
+ @objc public enum VibesStorageEnum: Int {
34
+ case KEYCHAIN
35
+ case USERDEFAULTS
36
+ }
37
+
38
+ private var vibesInstance: Vibes!
39
+
40
+ // swiftlint:disable type_body_length
41
+ /// The main entry point for usage of the Vibes API.
42
+ @objc public class Vibes: NSObject, EventTracker {
43
+ /// The released version of the Vibes SDK
44
+ public static let SDK_VERSION = "5.0.0"
45
+
46
+ /// The app ID
47
+ public static let CURRENT_APP_ID = "CURRENT_APP_ID"
48
+
49
+ /// Default configuration
50
+ public let configuration: VibesConfiguration
51
+
52
+ /// Register push status
53
+ public static let REGISTER_PUSH_STATUS = "VibesRegisterPushStatus"
54
+
55
+ /// An object that can communicate with the Vibes API.
56
+ internal var api: VibesAPIType
57
+
58
+ /// The API to use when communicating tracking info with Vibes.
59
+ internal var trackingApi: VibesAPIType
60
+
61
+ // The application ID provided by Vibes to identify this application.
62
+ private let appId: String
63
+
64
+ /// An object that handles managing credentials
65
+ let credentialManager: CredentialManager
66
+
67
+ /// An object that allows for persisting data locally
68
+ internal var storage: LocalStorageType
69
+
70
+ /// Queue for executing http requests
71
+ fileprivate lazy var queue: OperationQueue = {
72
+ let operationQueue = OperationQueue()
73
+ operationQueue.maxConcurrentOperationCount = 1 // Serial queue operation
74
+ return operationQueue
75
+ }()
76
+
77
+ /// List of Vibes delegates
78
+ @objc public weak var delegate: VibesAPIDelegate?
79
+
80
+ /// Logger for the configured singleton, or `nil` before `configure`.
81
+ internal static var configuredLogger: CombinedLogger? {
82
+ return vibesInstance?.configuration.logger
83
+ }
84
+
85
+ /// The shared (singleton) instance of Vibes.
86
+ @objc public internal(set) static var shared: Vibes! {
87
+ get {
88
+ if vibesInstance == nil {
89
+ fatalError("You must call Vibes.configure before using Vibes.shared")
90
+ }
91
+ return vibesInstance
92
+ }
93
+
94
+ set(newValue) {
95
+ vibesInstance = newValue
96
+ }
97
+ }
98
+
99
+ /// The currently set push token, if available. This is stored in local
100
+ /// storage.
101
+ @objc public internal(set) var pushToken: String? {
102
+ get {
103
+ return storage.get(LocalStorageKeys.pushToken)
104
+ }
105
+
106
+ set(newToken) {
107
+ storage.set(newToken, for: LocalStorageKeys.pushToken)
108
+ }
109
+ }
110
+
111
+ // A boolean to show pushToken needs to be updated.
112
+ /// Boolean indicating whether our servers should be notified of any changes to the APNS token.
113
+ /// This defaults to true, such that calling `setPushToken:fromData` or `setPushToken:token`
114
+ /// will automatically notify our server of the new change.
115
+ /// To disable this behavior, the app developer should call `disablePush()` before updating the
116
+ /// APNS token, and call `registerPush()` when appropriate.
117
+ private var isPushEnabled: Bool = true
118
+
119
+ /// Trick to detect wheter the application has been started for
120
+ /// executing unit tests. In that case, we don't want to send
121
+ /// events to Vibes backend.
122
+ public static let isRunningThroughUnitTest: Bool = {
123
+ if let testClass = NSClassFromString("XCTestCase") {
124
+ return true
125
+ } else {
126
+ return false
127
+ }
128
+ }()
129
+
130
+ /// An observer of the application lifecycle. This is used for automatic
131
+ /// tracking of launch and clickthru events.
132
+ private let appObserver: AppObserver
133
+
134
+ /// A utility object to persist events locally until such time as they can be
135
+ /// sent to Vibes.
136
+ private let eventStorage: PersistentEventStorage
137
+
138
+ fileprivate var credential: VibesCredential? {
139
+ return credentialManager.currentCredential
140
+ }
141
+
142
+ ///
143
+ /// Add a new Vibes delegate to the list of current delegates.
144
+ /// - parameters:
145
+ /// - delegate: VibesAPIDelegate
146
+ ///
147
+ @objc public func set(delegate: VibesAPIDelegate) {
148
+ self.delegate = delegate
149
+ }
150
+
151
+ ///
152
+ /// Get the storage according the storage type choosen by the user.
153
+ /// - parameters:
154
+ /// - type: VibesStorageType
155
+ /// - returns:
156
+ /// - LocalStorageType
157
+ ///
158
+ internal class func getStorageWith(type: VibesStorageEnum) -> LocalStorageType {
159
+ switch type {
160
+ case .KEYCHAIN: return KeychainStorage(service: "Vibes")
161
+ case .USERDEFAULTS: return UserDefaultsStorage()
162
+ }
163
+ }
164
+
165
+ ///
166
+ /// Initialize this object.
167
+ ///
168
+ /// - parameters:
169
+ /// - appId: The application ID provided by Vibes to identify this
170
+ /// application
171
+ /// - configuration: The Vibes Configuration object
172
+ init(appId: String,
173
+ configuration: VibesConfiguration? = nil) {
174
+ self.configuration = configuration ?? VibesConfiguration()
175
+ self.appId = appId
176
+ let storageToUse = type(of: self).getStorageWith(type: self.configuration.storageType)
177
+ storage = storageToUse
178
+ credentialManager = CredentialManager(storage: storageToUse)
179
+ eventStorage = PersistentEventStorage(storage: storageToUse)
180
+
181
+ api = VibesAPI(url: self.configuration.vibesAPIURL)
182
+
183
+ trackingApi = VibesAPI(url: self.configuration.vibesTrackingAPIURL)
184
+
185
+ let userDefaults = UserDefaults.standard
186
+
187
+ if userDefaults.bool(forKey: "hasRunBefore") == false {
188
+ // Remove Keychain item in case the application has been reinstalled.
189
+ // When an app is removed, its data in the keychain remains.
190
+ storage.set(nil, for: LocalStorageKeys.currentCredential)
191
+ userDefaults.set(true, forKey: "hasRunBefore")
192
+ userDefaults.synchronize()
193
+ }
194
+
195
+ let trackedEventTypes = self.configuration.trackedEventTypes as? [TrackedEventType] ?? []
196
+ appObserver = AppObserver(trackedEventTypes: trackedEventTypes)
197
+ super.init()
198
+ if !Vibes.isRunningThroughUnitTest {
199
+ appObserver.eventTracker = self
200
+ }
201
+ }
202
+
203
+ ///
204
+ /// Enables register push
205
+ /// This will result in sending the APNS token to our servers whenever it changes
206
+ ///
207
+ @objc public func enablePush() {
208
+ isPushEnabled = true
209
+ }
210
+
211
+ ///
212
+ /// Disable register push
213
+ /// This will result in NOT sending the APNS token to our servers whenever it changes.
214
+ /// By calling this, it will be up to the app developer to call registerPush() when appropriate.
215
+ ///
216
+ @objc public func disablePush() {
217
+ isPushEnabled = false
218
+ }
219
+
220
+ ///
221
+ /// Get the status of the device registration
222
+ /// - returns: Boolean
223
+ ///
224
+ @objc public func isDeviceRegistered() -> Bool {
225
+ return credentialManager.currentCredential != nil
226
+ }
227
+
228
+ ///
229
+ /// Get the status of the device push registration
230
+ /// - returns: Boolean
231
+ ///
232
+ @objc public func isDevicePushRegistered() -> Bool {
233
+ return UserDefaults.standard.bool(forKey: Vibes.REGISTER_PUSH_STATUS)
234
+ }
235
+
236
+ ///
237
+ /// Parses the push token from the Data that Apple sends, and stores it
238
+ /// locally by converting the data to a hex string. Once this has been called,
239
+ /// `registerPush` can be called at will to enable push notifications from
240
+ /// Vibes.
241
+ ///
242
+ /// - parameters:
243
+ /// - fromData: the data send from Apple and received in your AppDelegate's
244
+ /// `application:didRegisterForRemoteNotificationsWithDeviceToken` function.
245
+ ///
246
+ @objc public func setPushToken(fromData data: Data) {
247
+ setPushToken(token: data.reduce("", { $0 + String(format: "%02X", $1) }))
248
+ }
249
+
250
+ ///
251
+ /// Stores Token in local storage and if Push is enabled it pushes to vibes
252
+ /// servers
253
+ /// - Parameters:
254
+ /// - Optional token that is set
255
+ ///
256
+ private func setPushToken(token: String?) {
257
+ storage.set(token, for: LocalStorageKeys.pushToken)
258
+ if isPushEnabled {
259
+ registerPush()
260
+ }
261
+ }
262
+
263
+ ///
264
+ /// Notify Vibes that a push message has been tapped on or for the case of silent push, has been received.
265
+ ///
266
+ /// - parameters:
267
+ /// - userInfo: the details from the remote notification
268
+ /// - timestamp: an optional date for the receipt of the push notification
269
+ /// (default: now)
270
+ ///
271
+ @objc public func receivedPush(with userInfo: [AnyHashable: Any], at timestamp: Date = Date()) {
272
+ appObserver.lastNotification = (userInfo, timestamp)
273
+ storeTrackingData(userInfo: userInfo)
274
+ handleSilentMigration(userInfo)
275
+ }
276
+
277
+ /// Handle Silent migration
278
+ ///
279
+ /// - Parameter userInfo: The push payload
280
+ func handleSilentMigration(_ userInfo: [AnyHashable: Any]) {
281
+ if let customData = userInfo["client_custom_data"] as? [String: Any], // ensure client_custom_data is supplied
282
+ let pushToken = customData["vibes_auto_register_token"] as? String, // ensure push token is supplied in client_custom_data
283
+ let migrationItemId = customData["migration_item_id"] as? String, // ensure the migration item id
284
+ !pushToken.isEmpty { // ensure push token is not empty
285
+ Vibes.shared.configuration.logger?
286
+ .log(LogObject(.info, message: "Migration push received. Processing ..."))
287
+ let registerDeviceCallback: VibesCompletion<VibesCredential> = { [weak self] result in
288
+ switch result {
289
+ case let .success(credential):
290
+ Vibes.shared.configuration.logger?
291
+ .log(LogObject(.info, message: "Migration request completed successfully. Device Id: \(credential.deviceId)"))
292
+ self?.setPushToken(token: pushToken)
293
+ self?.registerPush()
294
+ // trigger migration callback
295
+ if let appId = self?.appId {
296
+ self?.migrationCallback(appId: appId, migrationItemId: migrationItemId, vibesDeviceId: credential.deviceId)
297
+ }
298
+ case let .failure(error):
299
+ Vibes.shared.configuration.logger?
300
+ .log(LogObject(.error, message: "Migration attempt failed in triggering registerDevice(): \(error)"))
301
+ }
302
+ }
303
+
304
+ submit(operation: RegisterDeviceOperation(credentials: credentialManager,
305
+ api: api,
306
+ advertisingId: advertisingId,
307
+ appId: appId,
308
+ delegate: delegate,
309
+ callback: registerDeviceCallback))
310
+ } else {
311
+ Vibes.shared.configuration.logger?
312
+ .log(LogObject(.info, message: "Silent Register Device Failed: Valid Push Token was not supplied"))
313
+ }
314
+ }
315
+
316
+ /// Trigger Migration Callback Http Operation
317
+ ///
318
+ /// - Parameters:
319
+ /// - appId: The app id
320
+ /// - migrationItemId: Migration item id
321
+ /// - vibesDeviceId: Vibes Device id
322
+ func migrationCallback(appId: String, migrationItemId: String, vibesDeviceId: String) {
323
+ let migrationCallback: VibesCompletion<Void> = { result in
324
+ switch result {
325
+ case .success:
326
+ Vibes.shared.configuration.logger?
327
+ .log(LogObject(.info, message: "Migration Callback Successfull."))
328
+ case let .failure(error):
329
+ Vibes.shared.configuration.logger?
330
+ .log(LogObject(.error, message: "Couldn't process migration callback: \(error)"))
331
+ }
332
+ }
333
+ submit(operation: MigrationCallbackOperation(credentials: credentialManager,
334
+ api: api,
335
+ advertisingId: advertisingId,
336
+ appId: appId,
337
+ migrationItemId: migrationItemId,
338
+ vibesDeviceId: vibesDeviceId,
339
+ callback: migrationCallback))
340
+ }
341
+
342
+ ///
343
+ /// Submit a new operation (REG_DEVICE, UNREG_DEVICE ...)
344
+ /// If the current queue isn't empty, the operation will be executed
345
+ /// when the last operation of the queue is finished.
346
+ ///
347
+ fileprivate func submit<A>(operation: BaseOperation<A>) {
348
+ if !queue.operations.isEmpty {
349
+ operation.addDependency(queue.operations.last!)
350
+ }
351
+ queue.addOperation(operation)
352
+ }
353
+
354
+ // MARK: OPERATIONS
355
+
356
+ ///////////////////////////////////////////// OPERATIONS /////////////////////////////////////////////
357
+
358
+ ///
359
+ /// Checks and compares the Vibes SDK version in use.
360
+ ///
361
+ internal func checkLatestVersion(_ callback: GetGitTagsCallback? = nil) {
362
+ let gitTagsCallback: GetGitTagsCallback = { tags, error in
363
+ if let error = error {
364
+ self.configuration.logger?
365
+ .log(LogObject(.error, message: "Unable to fetch latest version of SDK --> \(error)"))
366
+ callback?(tags, error)
367
+ return
368
+ }
369
+ if let latest = tags.first {
370
+ if latest.name.compare(Vibes.SDK_VERSION) == .orderedDescending {
371
+ self.configuration.logger?
372
+ .log(LogObject(.info, message: "Latest version of the Vibes SDK is v\(latest.name). We recommend upgrading by following the instructions here: https://developer.vibes.com/display/APIs/Installing+the+iOS+Push+Notifications+SDK."))
373
+ } else {
374
+ self.configuration.logger?
375
+ .log(LogObject(.info, message: "Your Vibes SDK v\(Vibes.SDK_VERSION) is up to date."))
376
+ }
377
+ }
378
+ callback?(tags, error)
379
+ }
380
+ submit(operation: GetGitTagsOperation(credentials: credentialManager,
381
+ storage: storage,
382
+ api: api,
383
+ advertisingId: advertisingId,
384
+ appId: appId,
385
+ callback: gitTagsCallback))
386
+ }
387
+
388
+ private var advertisingId: String {
389
+ return configuration.advertisingId ?? ""
390
+ }
391
+
392
+ ///
393
+ /// Registers this device with Vibes.
394
+ ///
395
+ @objc public func registerDevice() {
396
+ let registerDeviceCallback: VibesCompletion<VibesCredential> = { [weak self] result in
397
+ switch result {
398
+ case .success:
399
+ self?.configuration.logger?.log(LogObject(.info, message: "Device registered successfully"))
400
+ case let .failure(error):
401
+ self?.configuration.logger?.log(LogObject(.error, message: "Device registered failed: \(error)"))
402
+ }
403
+ }
404
+
405
+ submit(operation: RegisterDeviceOperation(credentials: credentialManager,
406
+ api: api,
407
+ advertisingId: advertisingId,
408
+ appId: appId,
409
+ delegate: delegate,
410
+ callback: registerDeviceCallback))
411
+ }
412
+
413
+ ///
414
+ /// Unregisters this device with Vibes.
415
+ ///
416
+ @objc public func unregisterDevice() {
417
+ let unRegisterDeviceCallback: VibesCompletion<Void> = { [weak self] result in
418
+ switch result {
419
+ case .success:
420
+ self?.configuration.logger?.log(LogObject(.info, message: "Device unregistered successfully"))
421
+ case let .failure(error):
422
+ self?.configuration.logger?.log(LogObject(.error, message: "Device unregistered failed: \(error)"))
423
+ }
424
+ }
425
+
426
+ submit(operation: UnregisterDeviceOperation(credentials: credentialManager,
427
+ api: api,
428
+ advertisingId: advertisingId,
429
+ appId: appId,
430
+ delegate: delegate,
431
+ callback: unRegisterDeviceCallback))
432
+ }
433
+
434
+ ///
435
+ /// Updates this device with Vibes.
436
+ ///
437
+ /// - parameters:
438
+ /// - lat: latitude of the User location.
439
+ /// - long: longitude of the User location
440
+ /// - updateCredentials: a boolean indicating if it's a token update or device info update. Specify `false` if not certain
441
+ ///
442
+ @objc public func updateDevice(lat: NSNumber? = nil, long: NSNumber? = nil, updateCredentials: Bool) {
443
+ let updateDeviceCallback: VibesCompletion<Void> = { [weak self] result in
444
+ switch result {
445
+ case .success:
446
+ self?.configuration.logger?.log(LogObject(.info, message: "Device updated successfully"))
447
+ case let .failure(error):
448
+ self?.configuration.logger?.log(LogObject(.error, message: "Update device failed: \(error)"))
449
+ }
450
+ }
451
+
452
+ var location: (Double, Double)?
453
+ if let lat = lat as? Double,
454
+ let long = long as? Double {
455
+ location = (lat: lat, long: long)
456
+ }
457
+
458
+ if updateCredentials {
459
+ submit(operation: UpdateDeviceOperationPut(credentials: credentialManager,
460
+ api: api,
461
+ advertisingId: advertisingId,
462
+ appId: appId,
463
+ delegate: delegate,
464
+ location: location,
465
+ callback: updateDeviceCallback))
466
+ } else {
467
+ submit(operation: UpdateDeviceOperationPatch(credentials: credentialManager,
468
+ api: api,
469
+ advertisingId: advertisingId,
470
+ appId: appId,
471
+ delegate: delegate,
472
+ location: location,
473
+ callback: updateDeviceCallback))
474
+ }
475
+ }
476
+
477
+ ///
478
+ /// Associate this device with a person
479
+ ///
480
+ /// - paramters:
481
+ /// - externalPersonId: the third party identifier for the person, stored
482
+ /// by vibes as external_person_id
483
+ @objc public func associatePerson(externalPersonId: String) {
484
+ let associatePersonCallback: VibesCompletion<Void> = { [weak self] result in
485
+ switch result {
486
+ case .success:
487
+ self?.configuration.logger?.log(LogObject(.info, message: "Associated person with device successfully"))
488
+ case let .failure(error):
489
+ self?.configuration.logger?.log(LogObject(.error, message: "Register Device failed: \(error)"))
490
+ }
491
+ }
492
+
493
+ submit(operation: AssociatePersonOperation(credentials: credentialManager,
494
+ api: api,
495
+ advertisingId: advertisingId,
496
+ appId: appId,
497
+ delegate: delegate,
498
+ callback: associatePersonCallback,
499
+ externalPersonId: externalPersonId))
500
+ }
501
+
502
+ ///
503
+ /// Register push notifications for this device with Vibes.
504
+ ///
505
+ @objc public func registerPush() {
506
+ let registerPushCallback: VibesCompletion<Void> = { [weak self] result in
507
+ switch result {
508
+ case .success:
509
+ self?.configuration.logger?.log(LogObject(.info, message: "Push token registered successfully"))
510
+ case let .failure(error):
511
+ self?.configuration.logger?.log(LogObject(.error, message: "Push token registration failed: \(error)"))
512
+ }
513
+ }
514
+
515
+ submit(operation: RegisterPushOperation(credentials: credentialManager,
516
+ storage: storage,
517
+ api: api,
518
+ advertisingId: advertisingId,
519
+ appId: appId,
520
+ delegate: delegate,
521
+ callback: registerPushCallback))
522
+ }
523
+
524
+ ///
525
+ /// Unregister push notifications for this device with Vibes.
526
+ ///
527
+ @objc public func unregisterPush() {
528
+ let unRegisterPushCallback: VibesCompletion<Void> = { [weak self] result in
529
+ switch result {
530
+ case .success:
531
+ self?.configuration.logger?.log(LogObject(.info, message: "Push token unregistered successfully"))
532
+ case let .failure(error):
533
+ self?.configuration.logger?.log(LogObject(.error, message: "Push token unregistered failed: \(error)"))
534
+ }
535
+ }
536
+
537
+ submit(operation: UnregisterPushOperation(credentials: credentialManager,
538
+ storage: storage,
539
+ api: api,
540
+ advertisingId: advertisingId,
541
+ appId: appId,
542
+ delegate: delegate,
543
+ callback: unRegisterPushCallback))
544
+ }
545
+
546
+ ///
547
+ /// Get Vibes Person Information.
548
+ ///
549
+ /// - Parameter callback: GetPersonCallback (Person?, Error?)
550
+ @objc public func getPerson(_ callback: GetPersonCallback?) {
551
+ submit(operation: GetPersonOperation(credentials: credentialManager,
552
+ storage: storage,
553
+ api: api,
554
+ advertisingId: advertisingId,
555
+ appId: appId,
556
+ callback: completionWithLogger(.getPerson, callback)))
557
+ }
558
+
559
+ ///
560
+ /// Retrieve inbox messages status for an app
561
+ ///
562
+ /// - Parameter callback: GetVibesAppInfoCallback (Status, Error?)
563
+ @objc public func getAppInfo(_ callback: GetVibesAppInfoCallback?) {
564
+ submit(operation: GetVibesAppInfoOperation(credentials: credentialManager,
565
+ storage: storage,
566
+ api: api,
567
+ advertisingId: advertisingId,
568
+ appId: appId,
569
+ callback: completionWithLogger(.getAppInfo, callback)))
570
+ }
571
+
572
+ /// Retrieve inbox messages for a Person
573
+ ///
574
+ /// - Parameters:
575
+ /// - callback: GetInboxMessagesCallback ([InboxMessage], Error?)
576
+ @objc public func fetchInboxMessages(_ callback: GetInboxMessagesCallback?) {
577
+ let getPersonCallback: GetPersonCallback = { person, error in
578
+ if let person = person, let personKey = person.personKey {
579
+ self.storage.set(person, for: LocalStorageKeys.currentPerson)
580
+ self.submit(operation: GetInboxMessagesOperation(credentials: self.credentialManager,
581
+ storage: self.storage,
582
+ api: self.api,
583
+ advertisingId: self.advertisingId,
584
+ appId: self.appId,
585
+ personKey: personKey,
586
+ callback: self.completionWithLogger(.fetchMessages, callback)))
587
+ } else {
588
+ callback?([], error) // error retrieving person info
589
+ }
590
+ }
591
+ submit(operation: GetPersonOperation(credentials: credentialManager,
592
+ storage: storage,
593
+ api: api,
594
+ advertisingId: advertisingId,
595
+ appId: appId,
596
+ callback: getPersonCallback))
597
+ }
598
+
599
+ /// Retrieve a single inbox message for a Person
600
+ ///
601
+ /// - Parameters:
602
+ /// - messageUID: unique UID for the message to be edited
603
+ /// - callback: GetInboxMessageCallback (InboxMessage, Error?)
604
+ @objc public func fetchInboxMessage(messageUID: String, _ callback: @escaping GetInboxMessageCallback) {
605
+ if let person = storage.get(LocalStorageKeys.currentPerson),
606
+ let personKey = person.personKey {
607
+ submit(operation: GetInboxMessageOperation(credentials: credentialManager,
608
+ storage: storage,
609
+ api: api,
610
+ advertisingId: advertisingId,
611
+ appId: appId,
612
+ personKey: personKey,
613
+ messageUID: messageUID,
614
+ callback: completionWithLogger(.fetchMessage, callback)))
615
+ } else { // we dint find personKey, lets retrieve it then retry
616
+ let getPersonCallback: GetPersonCallback = { person, error in
617
+ if let person = person, let personKey = person.personKey {
618
+ self.storage.set(person, for: LocalStorageKeys.currentPerson)
619
+ self.submit(operation: GetInboxMessageOperation(credentials: self.credentialManager,
620
+ storage: self.storage,
621
+ api: self.api,
622
+ advertisingId: self.advertisingId,
623
+ appId: self.appId,
624
+ personKey: personKey,
625
+ messageUID: messageUID,
626
+ callback: callback))
627
+ } else {
628
+ callback(nil, error) // error retrieving person info
629
+ }
630
+ }
631
+ submit(operation: GetPersonOperation(credentials: credentialManager,
632
+ storage: storage,
633
+ api: api,
634
+ advertisingId: advertisingId,
635
+ appId: appId,
636
+ callback: getPersonCallback))
637
+ }
638
+ }
639
+
640
+ /// Mark inbox message as read
641
+ ///
642
+ /// - Parameters:
643
+ /// - messageUID: unique UID for the message to be edited
644
+ /// - callback: UpdateMessageCallback (InboxMessage?, Error?)
645
+ @objc public func markInboxMessageAsRead(messageUID: String, _ callback: @escaping UpdateMessageCallback) {
646
+ updateInboxMessage(messageUID: messageUID,
647
+ payload: ["read": true as AnyObject],
648
+ callback: completionWithLogger(.markMessageRead, callback))
649
+ }
650
+
651
+ /// Expire inbox message at specified date
652
+ ///
653
+ /// - Parameters:
654
+ /// - messageUID: unique UID for the message to be edited
655
+ /// - date: The date to expire the inbox message. if not set, defaults to now.
656
+ /// - callback: UpdateMessageCallback (InboxMessage?, Error?)
657
+ @objc public func expireInboxMessage(messageUID: String, date: Date = Date(), _ callback: @escaping UpdateMessageCallback) {
658
+ updateInboxMessage(messageUID: messageUID,
659
+ payload: ["expires_at": date.iso8601WithUpdatingTimezone as AnyObject],
660
+ callback: completionWithLogger(.markMessageExpired, callback))
661
+ }
662
+
663
+ /// Records the fact that an inbox message has been viewed in detail by the user.
664
+ ///
665
+ /// - Parameter inboxMessage: The viewed inbox message
666
+ @objc public func onInboxMessageOpen(inboxMessage: InboxMessage) {
667
+ let event = Event(eventType: .inboxopen, properties: inboxMessage.eventMap())
668
+ track(event: event)
669
+ }
670
+
671
+ /// Records the fact that an inbox messages have been fetched by the user.
672
+ ///
673
+ @objc public func onInboxMessagesFetched() {
674
+ let event = Event(eventType: .inboxfetch, properties: [:])
675
+ track(event: event)
676
+ }
677
+
678
+ /// Update inbox message
679
+ ///
680
+ /// - Parameters:
681
+ /// - messageUID: unique UID for the message to be edited
682
+ /// - payload: payload to be updated in the backend
683
+ /// - callback: UpdateMessageCallback (InboxMessage?, Error?)
684
+ internal func updateInboxMessage(messageUID: String, payload: VibesJSONDictionary, callback: @escaping UpdateMessageCallback) {
685
+ // callback, here on success, we use person key to fetch inbox messages
686
+ if let person = storage.get(LocalStorageKeys.currentPerson),
687
+ let personKey = person.personKey {
688
+ submit(operation: UpdateMessageOperation(credentials: credentialManager,
689
+ storage: storage,
690
+ api: api,
691
+ advertisingId: advertisingId,
692
+ appId: appId,
693
+ personKey: personKey,
694
+ messageUID: messageUID,
695
+ payload: payload,
696
+ callback: callback))
697
+ } else { // we dint find personKey, lets retrieve it then retry
698
+ let getPersonCallback: GetPersonCallback = { person, error in
699
+ if let person = person, let personKey = person.personKey {
700
+ self.storage.set(person, for: LocalStorageKeys.currentPerson)
701
+ self.submit(operation: UpdateMessageOperation(credentials: self.credentialManager,
702
+ storage: self.storage,
703
+ api: self.api,
704
+ advertisingId: self.advertisingId,
705
+ appId: self.appId,
706
+ personKey: personKey,
707
+ messageUID: messageUID,
708
+ payload: payload,
709
+ callback: callback))
710
+ } else {
711
+ callback(nil, error) // error retrieving person info
712
+ }
713
+ }
714
+ submit(operation: GetPersonOperation(credentials: credentialManager,
715
+ storage: storage,
716
+ api: api,
717
+ advertisingId: advertisingId,
718
+ appId: appId,
719
+ callback: getPersonCallback))
720
+ }
721
+ }
722
+
723
+ ///
724
+ /// Tracks events with Vibes.
725
+ ///
726
+ /// - parameters:
727
+ /// - events: the Events to track, e.g. [.launch]
728
+ /// - completion: a handler for receiving the result of tracking the events
729
+ /// (it returns an Array of the events that have been tracked)
730
+ ///
731
+ internal func track(events incomingEvents: [Event], completion: VibesCompletion<[Event]>?) {
732
+ let allowedEvents = handleDuplicateLaunchEventsBySecond(events: incomingEvents, existing: eventStorage.storedEvents)
733
+ eventStorage.tracking(events: allowedEvents) { outgoingEvents, trackingCompletion in
734
+ self.submit(operation: EventOperation(credentials: self.credentialManager,
735
+ api: self.api,
736
+ advertisingId: advertisingId,
737
+ appId: self.appId,
738
+ completion: completion,
739
+ outgoingEvents: outgoingEvents,
740
+ trackCompletion: trackingCompletion))
741
+ }
742
+ }
743
+
744
+ /// Track CLICK Product Action
745
+ ///
746
+ /// - Parameters:
747
+ /// - product: the product being tracked
748
+ /// - activityUid: optional activityUid
749
+ /// - completion: Completion Callback
750
+ @objc public func trackClick(product: Product, activityUid: String? = nil, completion: TrackProductActionCallback?) {
751
+ trackActionCall(action: ProductAction.click, product: product, activityUid: activityUid, completion: completionWithLogger(.trackProductClick, completion))
752
+ }
753
+
754
+ /// Track ADD Product Action
755
+ ///
756
+ /// - Parameters:
757
+ /// - product: the product being tracked
758
+ /// - activityUid: optional activityUid
759
+ /// - completion: Completion Callback
760
+ @objc public func trackAdd(product: Product, activityUid: String? = nil, completion: TrackProductActionCallback?) {
761
+ trackActionCall(action: ProductAction.add, product: product, activityUid: activityUid, completion: completionWithLogger(.trackProductAdd, completion))
762
+ }
763
+
764
+ /// Track Product Action Call
765
+ ///
766
+ /// - Parameters:
767
+ /// - action: the type of ProductAction
768
+ /// - product: the product being tracked
769
+ /// - activityUid: optional activityUid
770
+ /// - completion: Completion Callback
771
+ internal func trackActionCall(action: ProductAction, product: Product, activityUid: String?, completion: TrackProductActionCallback?) {
772
+ if let trackingData = getTrackingData() {
773
+ submit(operation: TrackProductActionOperation(credentials: credentialManager, storage: storage, api: trackingApi, advertisingId: advertisingId, appId: appId, action: action, product: product, data: trackingData, activityUid: activityUid, callback: completion))
774
+ } else {
775
+ completion?(VibesError.other("No Tracking Data"))
776
+ }
777
+ }
778
+
779
+ /// Track Detail
780
+ ///
781
+ /// - Parameters:
782
+ /// - product: the product being tracked
783
+ /// - activityUid: optional activityUid
784
+ /// - completion: Completion Callback
785
+ @objc public func trackDetail(product: Product, activityUid: String? = nil, completion: TrackProductActionCallback?) {
786
+ trackProductActionCall(action: .detail, product: product, activityUid: activityUid, completion: completionWithLogger(.trackDetail, completion))
787
+ }
788
+
789
+ internal enum SDKAction<T> {
790
+ case fetchMessage
791
+ case fetchMessages
792
+ case markMessageRead
793
+ case markMessageExpired
794
+ case getPerson
795
+ case getAppInfo
796
+ case trackProductClick
797
+ case trackProductAdd
798
+ case trackDetail
799
+
800
+ func successLogObject(_ data: T) -> LogObject {
801
+ switch self {
802
+ case .fetchMessage:
803
+ return LogObject(.info, message: "Inbox Message fetched successfully")
804
+ case .fetchMessages:
805
+ return LogObject(.info, message: "Inbox messages fetched successfully")
806
+ case .markMessageRead:
807
+ return LogObject(.info, message: "Inbox message marked as read successfully")
808
+ case .markMessageExpired:
809
+ return LogObject(.info, message: "Inbox message marked as expired successfully")
810
+ case .getPerson:
811
+ return LogObject(.info, message: "Successfully retrieved person details")
812
+ case .getAppInfo:
813
+ if let info = data as? VibesAppInfo {
814
+ let state = info.inboxEnabled ? "enabled" : "disabled"
815
+ return LogObject(.info, message: "App Inbox is \(state).")
816
+ }
817
+ return LogObject(.info, message: "App Inbox status unknown.")
818
+ case .trackProductClick:
819
+ return LogObject(.info, message: "Product CLICK tracked successfully")
820
+ case .trackProductAdd:
821
+ return LogObject(.info, message: "Product ADD tracked successfully")
822
+ case .trackDetail:
823
+ return LogObject(.info, message: "Product DETAIL tracked successfully")
824
+ }
825
+ }
826
+
827
+ func errorLogObject(_ error: Error) -> LogObject {
828
+ switch self {
829
+ case .fetchMessage:
830
+ return LogObject(.error, message: "Fetch Messsage Failed: \(error.localizedDescription)")
831
+ case .fetchMessages:
832
+ return LogObject(.error, message: "Fetch Messsages Failed: \(error)")
833
+ case .markMessageRead:
834
+ return LogObject(.error, message: "Inbox message marked as read failed: \(error)")
835
+ case .markMessageExpired:
836
+ return LogObject(.error, message: "Inbox message marked as expired failed: \(error)")
837
+ case .getPerson:
838
+ return LogObject(.error, message: "Retrieve person details failed: \(error)")
839
+ case .getAppInfo:
840
+ return LogObject(.error, message: "Retrieve Vibes App Info failed: \(error)")
841
+ case .trackProductClick:
842
+ return LogObject(.error, message: "Product CLICK tracking failed: \(error)")
843
+ case .trackProductAdd:
844
+ return LogObject(.error, message: "Product ADD tracking failed: \(error)")
845
+ case .trackDetail:
846
+ return LogObject(.error, message: "Product DETAIL tracking failed: \(error)")
847
+ }
848
+ }
849
+ }
850
+
851
+ // MARK: Tracking
852
+
853
+ /// Wraps a callback to handle logging
854
+ ///
855
+ /// - Parameters:
856
+ /// - action: SDK Action to log
857
+ /// - callback: The callback to wrap
858
+ /// - Returns: The callback with logging capabilities
859
+ internal func completionWithLogger<T>(_ action: SDKAction<T>, _ callback: ((T, Error?) -> Void)?) -> (T, Error?) -> Void {
860
+ let completion: (T, Error?) -> Void = { (result: T, error: Error?) in
861
+ if let error = error {
862
+ self.configuration.logger?.log(action.errorLogObject(error))
863
+ callback?(result, error)
864
+ return
865
+ }
866
+ self.configuration.logger?.log(action.successLogObject(result))
867
+ callback?(result, error)
868
+ }
869
+ return completion
870
+ }
871
+
872
+ /// Wraps a void callback to handle logging
873
+ ///
874
+ /// - Parameters:
875
+ /// - action: SDK Action to log
876
+ /// - callback: The callback to wrap
877
+ /// - Returns: The callback with logging capabilities
878
+ internal func completionWithLogger(_ action: SDKAction<Void>, _ callback: ((Error?) -> Void)?) -> (Error?) -> Void {
879
+ let completion: (Error?) -> Void = { (error: Error?) in
880
+ if let error = error {
881
+ self.configuration.logger?.log(action.errorLogObject(error))
882
+ callback?(error)
883
+ return
884
+ }
885
+ self.configuration.logger?.log(action.successLogObject(()))
886
+ callback?(error)
887
+ }
888
+ return completion
889
+ }
890
+
891
+ /// Store Tracking Data
892
+ ///
893
+ /// - Parameters:
894
+ /// - userInfo: Client_app_data
895
+ @objc fileprivate func storeTrackingData(userInfo: [AnyHashable: Any]) {
896
+ let getPersonCallback: GetPersonCallback = { person, _ in
897
+ if let person = person, let personKey = person.personKey {
898
+ if var clientAppData = userInfo["client_app_data"] as? VibesTrackingData {
899
+ clientAppData["person_key"] = personKey
900
+ let userDefaults = UserDefaults.standard
901
+ userDefaults.set(clientAppData, forKey: "vibesTagData")
902
+ }
903
+ }
904
+ }
905
+ getPerson(getPersonCallback)
906
+ }
907
+
908
+ /// Get Tracking Data
909
+ ///
910
+ /// - Returns: VibesTrackingData
911
+ @objc public func getTrackingData() -> VibesTrackingData? {
912
+ let userDefaults = UserDefaults.standard
913
+ return userDefaults.object(forKey: "vibesTagData") as? VibesTrackingData
914
+ }
915
+
916
+ /// Track PURCHASE Product Action
917
+ ///
918
+ /// - Parameters:
919
+ /// - activityUid: optional activityUid
920
+ /// - completion: Completion Callback
921
+ @objc public func trackPurchase(purchase: Purchase, activityUid: String?, completion: TrackProductActionCallback?) {
922
+ trackPurchaseActionCall(action: PurchaseAction.purchase,
923
+ purchase: purchase,
924
+ activityUid: activityUid,
925
+ completion: completion)
926
+ }
927
+
928
+ /// Track Product Action Call
929
+ ///
930
+ /// - Parameters:
931
+ /// - action: the type of Product Action
932
+ /// - product: the product being tracked
933
+ /// - activityUid: optional activityUid
934
+ /// - completion: Completion Callback
935
+ internal func trackProductActionCall(action: ProductAction, product: Product, activityUid: String?, completion: TrackProductActionCallback?) {
936
+ if let trackingData = getTrackingData() {
937
+ submit(operation: TrackProductActionOperation(credentials: credentialManager,
938
+ storage: storage,
939
+ api: trackingApi,
940
+ advertisingId: advertisingId,
941
+ appId: appId,
942
+ action: action,
943
+ product: product,
944
+ data: trackingData,
945
+ activityUid: activityUid,
946
+ callback: completion))
947
+ } else {
948
+ completion?(VibesError.other("No Tracking Data"))
949
+ }
950
+ }
951
+
952
+ /// Track Purchase Action Call
953
+ ///
954
+ /// - Parameters:
955
+ /// - action: the type of purchase Action
956
+ /// - activityUid: optional activityUid
957
+ /// - completion: Completion Callback
958
+ internal func trackPurchaseActionCall(action: PurchaseAction, purchase: Purchase, activityUid: String?, completion: TrackPurchaseActionCallback?) {
959
+ if let trackingData = getTrackingData() {
960
+ submit(operation: TrackPurchaseActionOperation(credentials: credentialManager,
961
+ storage: storage,
962
+ api: trackingApi,
963
+ advertisingId: advertisingId,
964
+ appId: appId,
965
+ action: action,
966
+ purchase: purchase,
967
+ data: trackingData,
968
+ activityUid: activityUid,
969
+ callback: completion))
970
+ } else {
971
+ completion?(VibesError.other("No Tracking Data"))
972
+ }
973
+ }
974
+ }
975
+
976
+ extension Vibes {
977
+ ///
978
+ /// Configure the shared Vibes instance. This must be called before using any
979
+ /// of the functionality of Vibes, like registering your device.
980
+ ///
981
+ /// - Parameters:
982
+ /// - appId: The Vibes App ID
983
+ /// - configuration: Contains all the properties to configure Vibes
984
+ ///
985
+ @objc public static func configure(appId: String, configuration: VibesConfiguration? = nil) {
986
+ guard !appId.isEmpty else {
987
+ fatalError("App ID must not be null or empty")
988
+ }
989
+
990
+ shared = Vibes(appId: appId, configuration: configuration)
991
+ Vibes.shared.configuration.logger?.log(LogObject(.info, message: "\(ConsoleLogger.versionInfoLogPrefix)\(Vibes.SDK_VERSION)"))
992
+ if _isDebugAssertConfiguration() {
993
+ Vibes.shared.checkLatestVersion()
994
+ }
995
+
996
+ let userDefaults = UserDefaults.standard
997
+ let currentAppId = userDefaults.string(forKey: CURRENT_APP_ID) ?? ""
998
+
999
+ if currentAppId != appId {
1000
+ // Save the new appId and log the new appId
1001
+ userDefaults.set(appId, forKey: CURRENT_APP_ID)
1002
+ userDefaults.synchronize()
1003
+ Vibes.shared.configuration.logger?.log(LogObject(.info, message: "AppId has changed to: \(appId)"))
1004
+ }
1005
+ }
1006
+
1007
+ /// Will first ensure the events are unique, stripped of any dupicates based on equality test.
1008
+ /// Will then go through events and compare with any existing stored events,
1009
+ /// and handle any duplicate based on seconds difference in timestamp
1010
+ ///
1011
+ /// - Parameters:
1012
+ /// - events: The events to check
1013
+ /// - existing: The existing events list to check against
1014
+ /// - Returns: Events with any duplicates removed
1015
+ internal func handleDuplicateLaunchEventsBySecond(events: [Event], existing: [Event]) -> [Event] {
1016
+ let uniqueEvents = events.firstUniqueElements
1017
+ // filter all stored launch events
1018
+ let storedLaunchSorted = existing.filter({ event -> Bool in
1019
+ event.isLaunchEvent
1020
+ }).sorted { $0.timestamp > $1.timestamp }
1021
+ // get first from sorted list, which should be the latest one
1022
+ if let latestStoredLaunchEvent = storedLaunchSorted.first {
1023
+ let launchEvents = uniqueEvents.filter { event -> Bool in
1024
+ event.isLaunchEvent
1025
+ }
1026
+ if launchEvents.isEmpty {
1027
+ return uniqueEvents
1028
+ }
1029
+ let otherEvents = uniqueEvents.filter { event -> Bool in
1030
+ !event.isLaunchEvent
1031
+ }
1032
+ // find the accepted launch events to keep
1033
+ let acceptedLaunch = launchEvents.filter { event -> Bool in
1034
+ if let seconds = latestStoredLaunchEvent.timestamp.secondsDifference(with: event.timestamp),
1035
+ seconds > 1 { // add it to accepted list
1036
+ return true
1037
+ }
1038
+ print("Filtered out a launch event, similar exists within the second: \(event.encodeJSON())")
1039
+ return false // else dont add to accepted list
1040
+ }
1041
+ let result = otherEvents + acceptedLaunch
1042
+ return result
1043
+ }
1044
+ return uniqueEvents
1045
+ }
1046
+ }
1047
+
1048
+ public extension Sequence where Element: Equatable {
1049
+ /// Get the unique elements off this array, based on Eqality test
1050
+ var firstUniqueElements: [Element] {
1051
+ reduce(into: []) { uniqueElements, element in
1052
+ if !uniqueElements.contains(element) {
1053
+ uniqueElements.append(element)
1054
+ }
1055
+ }
1056
+ }
1057
+ }
1058
+
1059
+ extension Date {
1060
+ /// Get seconds difference between two dates
1061
+ /// - Parameter date: The other date
1062
+ /// - Returns: Seconds difference
1063
+ func secondsDifference(with date: Date) -> Int? {
1064
+ let calendar = Calendar.current
1065
+ let components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: self, to: date)
1066
+ let seconds = components.second
1067
+ return seconds
1068
+ }
1069
+ }