spotny-sdk 0.3.9 → 0.4.0
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.
Potentially problematic release.
This version of spotny-sdk might be problematic. Click here for more details.
|
@@ -11,6 +11,7 @@ import Foundation
|
|
|
11
11
|
import CoreLocation
|
|
12
12
|
import KontaktSDK
|
|
13
13
|
import UserNotifications
|
|
14
|
+
import Security
|
|
14
15
|
|
|
15
16
|
// MARK: - Public ObjC-visible typealias for the event callback block
|
|
16
17
|
|
|
@@ -69,6 +70,7 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
69
70
|
private var lastImpressionEventSent: [String: Date] = [:]
|
|
70
71
|
private var lastCampaignFetchAttempt: [String: Date] = [:]
|
|
71
72
|
private var fetchInProgress: [String: Bool] = [:]
|
|
73
|
+
private var fetchRetryCount: [String: Int] = [:]
|
|
72
74
|
private var proximityEventInProgress: [String: Bool] = [:]
|
|
73
75
|
private var impressionEventInProgress: [String: Bool] = [:]
|
|
74
76
|
|
|
@@ -227,6 +229,7 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
227
229
|
) {
|
|
228
230
|
lastCampaignFetchAttempt.removeAll()
|
|
229
231
|
fetchInProgress.removeAll()
|
|
232
|
+
fetchRetryCount.removeAll()
|
|
230
233
|
resolve("Debounce cache cleared")
|
|
231
234
|
}
|
|
232
235
|
|
|
@@ -302,13 +305,51 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
302
305
|
|
|
303
306
|
private func beaconKey(major: Int, minor: Int) -> String { "\(major)_\(minor)" }
|
|
304
307
|
|
|
308
|
+
// ── Keychain helpers (device ID survives uninstall/reinstall) ──────────────
|
|
309
|
+
private let keychainService = "app.spotny.sdk"
|
|
310
|
+
|
|
311
|
+
private func keychainRead(key: String) -> String? {
|
|
312
|
+
let query: [CFString: Any] = [
|
|
313
|
+
kSecClass: kSecClassGenericPassword,
|
|
314
|
+
kSecAttrService: keychainService,
|
|
315
|
+
kSecAttrAccount: key,
|
|
316
|
+
kSecReturnData: true,
|
|
317
|
+
kSecMatchLimit: kSecMatchLimitOne
|
|
318
|
+
]
|
|
319
|
+
var result: AnyObject?
|
|
320
|
+
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
|
|
321
|
+
let data = result as? Data else { return nil }
|
|
322
|
+
return String(data: data, encoding: .utf8)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private func keychainWrite(key: String, value: String) {
|
|
326
|
+
guard let data = value.data(using: .utf8) else { return }
|
|
327
|
+
let query: [CFString: Any] = [
|
|
328
|
+
kSecClass: kSecClassGenericPassword,
|
|
329
|
+
kSecAttrService: keychainService,
|
|
330
|
+
kSecAttrAccount: key
|
|
331
|
+
]
|
|
332
|
+
if SecItemCopyMatching(query as CFDictionary, nil) == errSecSuccess {
|
|
333
|
+
SecItemUpdate(query as CFDictionary, [kSecValueData: data] as CFDictionary)
|
|
334
|
+
} else {
|
|
335
|
+
var item = query; item[kSecValueData] = data
|
|
336
|
+
SecItemAdd(item as CFDictionary, nil)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
305
340
|
private func getDeviceId() -> String {
|
|
306
|
-
|
|
307
|
-
|
|
341
|
+
let kcKey = "SpotnySDK_deviceId"
|
|
342
|
+
// 1. Keychain — survives uninstall
|
|
343
|
+
if let stored = keychainRead(key: kcKey) { return stored }
|
|
344
|
+
// 2. Migrate existing UserDefaults value on first run after upgrade
|
|
345
|
+
if let legacy = UserDefaults.standard.string(forKey: kcKey) {
|
|
346
|
+
keychainWrite(key: kcKey, value: legacy)
|
|
347
|
+
UserDefaults.standard.removeObject(forKey: kcKey)
|
|
348
|
+
return legacy
|
|
308
349
|
}
|
|
350
|
+
// 3. Generate new ID
|
|
309
351
|
let id = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
|
|
310
|
-
|
|
311
|
-
UserDefaults.standard.synchronize()
|
|
352
|
+
keychainWrite(key: kcKey, value: id)
|
|
312
353
|
return id
|
|
313
354
|
}
|
|
314
355
|
|
|
@@ -375,13 +416,22 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
375
416
|
|
|
376
417
|
// MARK: - Campaign Fetching
|
|
377
418
|
|
|
419
|
+
/// Exponential cooldown: 5 s → 15 s → 45 s. After 3 failures the key is
|
|
420
|
+
/// considered permanently failed until the beacon region is re-entered.
|
|
421
|
+
private func fetchCooldown(for key: String) -> TimeInterval {
|
|
422
|
+
let retries = fetchRetryCount[key] ?? 0
|
|
423
|
+
return campaignFetchCooldown * pow(3.0, Double(min(retries, 3)))
|
|
424
|
+
}
|
|
425
|
+
|
|
378
426
|
private func fetchCampaign(major: Int, minor: Int, deviceId: String) {
|
|
379
427
|
let key = beaconKey(major: major, minor: minor)
|
|
380
428
|
guard activeCampaigns[key] == nil else { return }
|
|
381
429
|
guard fetchInProgress[key] != true else { return }
|
|
430
|
+
// Give up after 3 consecutive failures (cooldown would be 135 s+)
|
|
431
|
+
if (fetchRetryCount[key] ?? 0) > 3 { return }
|
|
382
432
|
|
|
383
433
|
if let last = lastCampaignFetchAttempt[key],
|
|
384
|
-
Date().timeIntervalSince(last) <
|
|
434
|
+
Date().timeIntervalSince(last) < fetchCooldown(for: key) { return }
|
|
385
435
|
|
|
386
436
|
lastCampaignFetchAttempt[key] = Date()
|
|
387
437
|
fetchInProgress[key] = true
|
|
@@ -398,6 +448,8 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
398
448
|
if case .success(let (s, _)) = result {
|
|
399
449
|
print("❌ SpotnySDK: Campaign fetch status \(s) for beacon \(key)")
|
|
400
450
|
}
|
|
451
|
+
// Increment retry count so next attempt uses a longer cooldown
|
|
452
|
+
self.fetchRetryCount[key] = (self.fetchRetryCount[key] ?? 0) + 1
|
|
401
453
|
return
|
|
402
454
|
}
|
|
403
455
|
do {
|
|
@@ -418,6 +470,7 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
418
470
|
self.activeCampaigns[key] = CampaignData(
|
|
419
471
|
campaignId: campaignId, screenId: screenId,
|
|
420
472
|
sessionId: nil, inQueue: inQueue, major: major, minor: minor)
|
|
473
|
+
self.fetchRetryCount.removeValue(forKey: key) // reset on success
|
|
421
474
|
print("✅ SpotnySDK: Campaign loaded for beacon \(key) — screenId=\(screenId)")
|
|
422
475
|
} catch {
|
|
423
476
|
print("❌ SpotnySDK: JSON parse error for beacon \(key): \(error)")
|
|
@@ -517,6 +570,7 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
517
570
|
lastImpressionEventSent.removeValue(forKey: key)
|
|
518
571
|
lastCampaignFetchAttempt.removeValue(forKey: key)
|
|
519
572
|
fetchInProgress.removeValue(forKey: key)
|
|
573
|
+
fetchRetryCount.removeValue(forKey: key)
|
|
520
574
|
proximityEventInProgress.removeValue(forKey: key)
|
|
521
575
|
impressionEventInProgress.removeValue(forKey: key)
|
|
522
576
|
print("🧹 SpotnySDK: Cleaned up state for beacon \(key)")
|
|
@@ -630,7 +684,9 @@ extension SpotnyBeaconScanner: KTKBeaconManagerDelegate {
|
|
|
630
684
|
let parts = region.identifier.components(separatedBy: "_")
|
|
631
685
|
if parts.count == 3, let major = Int(parts[1]), let minor = Int(parts[2]) {
|
|
632
686
|
let key = beaconKey(major: major, minor: minor)
|
|
687
|
+
// Reset fetch state on re-entry so retry backoff starts fresh
|
|
633
688
|
lastCampaignFetchAttempt.removeValue(forKey: key)
|
|
689
|
+
fetchRetryCount.removeValue(forKey: key)
|
|
634
690
|
if activeCampaigns[key] == nil {
|
|
635
691
|
fetchCampaign(major: major, minor: minor, deviceId: getDeviceId())
|
|
636
692
|
}
|