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
- if let stored = UserDefaults.standard.string(forKey: "SpotnySDK_deviceId") {
307
- return stored
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
- UserDefaults.standard.set(id, forKey: "SpotnySDK_deviceId")
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) < campaignFetchCooldown { return }
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spotny-sdk",
3
- "version": "0.3.9",
3
+ "version": "0.4.0",
4
4
  "description": "Beacon Scanner",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",