spotny-sdk 0.5.5 → 0.6.2
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.
- package/README.md +25 -17
- package/android/src/main/java/com/spotnysdk/SpotnySdkModule.kt +46 -26
- package/ios/SpotnyBeaconScanner.swift +53 -26
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +7 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +7 -2
package/README.md
CHANGED
|
@@ -65,12 +65,18 @@ import {
|
|
|
65
65
|
export default function App() {
|
|
66
66
|
useEffect(() => {
|
|
67
67
|
// 1. Initialize once before anything else
|
|
68
|
-
initialize({
|
|
68
|
+
initialize({
|
|
69
|
+
token: 'YOUR_SDK_TOKEN', // required — from your Spotny dashboard
|
|
70
|
+
source: 'nike',
|
|
71
|
+
maxDetectionDistance: 10,
|
|
72
|
+
});
|
|
69
73
|
|
|
70
74
|
// 2. Listen for nearby beacons
|
|
71
75
|
const beaconSub = addBeaconsRangedListener(({ beacons, region }) => {
|
|
72
76
|
beacons.forEach((b) =>
|
|
73
|
-
console.log(
|
|
77
|
+
console.log(
|
|
78
|
+
`${b.major}/${b.minor} — ${b.distance.toFixed(1)}m (${b.proximity})`
|
|
79
|
+
)
|
|
74
80
|
);
|
|
75
81
|
});
|
|
76
82
|
|
|
@@ -103,16 +109,18 @@ export default function App() {
|
|
|
103
109
|
|
|
104
110
|
**Must be called before any other SDK function.** Configures the SDK with your brand settings.
|
|
105
111
|
|
|
106
|
-
| Option
|
|
107
|
-
|
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
112
|
+
| Option | Type | Default | Description |
|
|
113
|
+
| -------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------- |
|
|
114
|
+
| `token` | `string` | **required** | SDK token issued by Spotny for your app. Verified against the backend — rejects with `UNAUTHORIZED` if invalid. |
|
|
115
|
+
| `source` | `string` | – | Your brand or app identifier (e.g. `'nike'`). Sent with every tracking event. |
|
|
116
|
+
| `maxDetectionDistance` | `number` | `8.0` | Maximum detection radius in metres. Beacons beyond this are ignored. |
|
|
117
|
+
| `distanceCorrectionFactor` | `number` | `0.5` | Multiplier applied to raw RSSI distance. Tune for your beacon TX power. |
|
|
111
118
|
|
|
112
|
-
Returns `Promise<string>`.
|
|
119
|
+
Returns `Promise<string>`. **Rejects** if the token is missing, invalid, or the network request fails — handle the error before calling `startScanner()`.
|
|
113
120
|
|
|
114
121
|
```ts
|
|
115
122
|
await initialize({
|
|
123
|
+
token: 'YOUR_SDK_TOKEN',
|
|
116
124
|
source: 'nike',
|
|
117
125
|
maxDetectionDistance: 10,
|
|
118
126
|
distanceCorrectionFactor: 0.5,
|
|
@@ -179,13 +187,13 @@ sub.remove(); // call on cleanup
|
|
|
179
187
|
|
|
180
188
|
Each beacon in the array:
|
|
181
189
|
|
|
182
|
-
| Field | Type | Description
|
|
183
|
-
| ----------- | -------- |
|
|
184
|
-
| `uuid` | `string` | Beacon proximity UUID
|
|
185
|
-
| `major` | `number` | Beacon major value
|
|
186
|
-
| `minor` | `number` | Beacon minor value
|
|
187
|
-
| `distance` | `number` | Estimated distance in metres (after correction factor applied)
|
|
188
|
-
| `rssi` | `number` | Raw signal strength in dBm
|
|
190
|
+
| Field | Type | Description |
|
|
191
|
+
| ----------- | -------- | -------------------------------------------------------------- |
|
|
192
|
+
| `uuid` | `string` | Beacon proximity UUID |
|
|
193
|
+
| `major` | `number` | Beacon major value |
|
|
194
|
+
| `minor` | `number` | Beacon minor value |
|
|
195
|
+
| `distance` | `number` | Estimated distance in metres (after correction factor applied) |
|
|
196
|
+
| `rssi` | `number` | Raw signal strength in dBm |
|
|
189
197
|
| `proximity` | `string` | `'immediate'` \| `'near'` \| `'far'` \| `'unknown'` |
|
|
190
198
|
|
|
191
199
|
---
|
|
@@ -196,8 +204,8 @@ Fires when the user enters or exits a beacon region. Works in foreground, backgr
|
|
|
196
204
|
|
|
197
205
|
```ts
|
|
198
206
|
const sub = addBeaconRegionListener(({ event, region, state }) => {
|
|
199
|
-
if (event === 'enter')
|
|
200
|
-
if (event === 'exit')
|
|
207
|
+
if (event === 'enter') console.log('Entered region', region);
|
|
208
|
+
if (event === 'exit') console.log('Left region', region);
|
|
201
209
|
if (event === 'determined') console.log('State for', region, '→', state);
|
|
202
210
|
});
|
|
203
211
|
|
|
@@ -49,6 +49,8 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
49
49
|
private var distanceCorrectionFactor = 0.5
|
|
50
50
|
/** Company/brand name identifying the SDK consumer (e.g. "nike"). */
|
|
51
51
|
private var source: String? = null
|
|
52
|
+
/** Bearer token used to authorise all API calls. Set during initialize(). */
|
|
53
|
+
private var sdkToken: String? = null
|
|
52
54
|
|
|
53
55
|
// ── Timing constants ──────────────────────────────────────────────────────
|
|
54
56
|
private var campaignFetchCooldown = 5_000L // ms
|
|
@@ -139,8 +141,32 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
139
141
|
config?.getDouble("distanceCorrectionFactor")
|
|
140
142
|
.takeIf { config?.hasKey("distanceCorrectionFactor") == true && it > 0 }
|
|
141
143
|
?.let { distanceCorrectionFactor = it; Log.d(TAG, "distanceCorrectionFactor = $it") }
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
val token = config?.getString("token")?.takeIf { it.isNotBlank() }
|
|
145
|
+
if (token == null) {
|
|
146
|
+
promise.reject("MISSING_TOKEN", "initialize() requires a token")
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
sdkToken = token
|
|
150
|
+
|
|
151
|
+
// Verify the token — device_id and source are sent as headers automatically
|
|
152
|
+
post("/api/app/sdk/verify", emptyMap()) { status, _ ->
|
|
153
|
+
when {
|
|
154
|
+
status in 200..299 -> {
|
|
155
|
+
Log.d(TAG, "SDK initialized and authorized (source: $source)")
|
|
156
|
+
promise.resolve("SDK initialized")
|
|
157
|
+
}
|
|
158
|
+
status == 401 || status == 403 -> {
|
|
159
|
+
sdkToken = null
|
|
160
|
+
Log.w(TAG, "Token unauthorized ($status)")
|
|
161
|
+
promise.reject("UNAUTHORIZED", "Invalid or expired SDK token")
|
|
162
|
+
}
|
|
163
|
+
else -> {
|
|
164
|
+
sdkToken = null
|
|
165
|
+
Log.w(TAG, "Verification failed ($status)")
|
|
166
|
+
promise.reject("VERIFY_FAILED", "SDK verification returned status $status")
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
144
170
|
}
|
|
145
171
|
|
|
146
172
|
override fun requestNotificationPermissions(promise: Promise) {
|
|
@@ -173,9 +199,7 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
173
199
|
|
|
174
200
|
override fun identify(userId: String, promise: Promise) {
|
|
175
201
|
identifiedUserId = userId
|
|
176
|
-
|
|
177
|
-
source?.let { payload["source"] = it }
|
|
178
|
-
post("/api/app/users/identify", payload) { status, _ ->
|
|
202
|
+
post("/api/app/users/identify", mapOf("user_id" to userId)) { status, _ ->
|
|
179
203
|
if (status in 200..299) {
|
|
180
204
|
Log.d(TAG, "Identified user $userId")
|
|
181
205
|
promise.resolve("User identified")
|
|
@@ -220,7 +244,6 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
220
244
|
// ── Range notifier ────────────────────────────────────────────────────────
|
|
221
245
|
|
|
222
246
|
private val rangeNotifier = RangeNotifier { beacons, region ->
|
|
223
|
-
val deviceId = getDeviceId()
|
|
224
247
|
val now = System.currentTimeMillis()
|
|
225
248
|
|
|
226
249
|
// JS event payload
|
|
@@ -260,13 +283,13 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
260
283
|
if (activeCampaigns.containsKey(key)) {
|
|
261
284
|
val isFirst = !lastProximityEventSent.containsKey(key)
|
|
262
285
|
if (isFirst) {
|
|
263
|
-
sendProximity("NEARBY", key, distance
|
|
286
|
+
sendProximity("NEARBY", key, distance)
|
|
264
287
|
lastProximityDistance[key] = distance
|
|
265
288
|
lastProximityEventSent[key] = now
|
|
266
289
|
} else if (distance >= 1.0) {
|
|
267
290
|
val lastDist = lastProximityDistance[key] ?: 0.0
|
|
268
291
|
if (abs(distance - lastDist) >= proximityDistanceThreshold) {
|
|
269
|
-
sendProximity("NEARBY", key, distance
|
|
292
|
+
sendProximity("NEARBY", key, distance)
|
|
270
293
|
lastProximityDistance[key] = distance
|
|
271
294
|
lastProximityEventSent[key] = now
|
|
272
295
|
}
|
|
@@ -275,12 +298,12 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
275
298
|
if (campaign?.campaignId != null && campaign.inQueue == false && distance <= impressionDistance) {
|
|
276
299
|
val lastImp = lastImpressionEventSent[key]
|
|
277
300
|
if (lastImp == null || now - lastImp >= impressionEventInterval) {
|
|
278
|
-
sendImpression(key, distance
|
|
301
|
+
sendImpression(key, distance)
|
|
279
302
|
lastImpressionEventSent[key] = now
|
|
280
303
|
}
|
|
281
304
|
}
|
|
282
305
|
} else {
|
|
283
|
-
fetchCampaign(major, minor
|
|
306
|
+
fetchCampaign(major, minor)
|
|
284
307
|
}
|
|
285
308
|
}
|
|
286
309
|
}
|
|
@@ -305,9 +328,8 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
305
328
|
putString("event", "exit")
|
|
306
329
|
}
|
|
307
330
|
sendEvent("onBeaconRegionEvent", payload)
|
|
308
|
-
val deviceId = getDeviceId()
|
|
309
331
|
for (key in activeCampaigns.keys.toList()) {
|
|
310
|
-
cleanupBeacon(key
|
|
332
|
+
cleanupBeacon(key)
|
|
311
333
|
}
|
|
312
334
|
try { beaconManager.stopRangingBeaconsInRegion(region) } catch (_: RemoteException) {}
|
|
313
335
|
}
|
|
@@ -362,8 +384,8 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
362
384
|
|
|
363
385
|
// ── State cleanup ─────────────────────────────────────────────────────────
|
|
364
386
|
|
|
365
|
-
private fun cleanupBeacon(key: String
|
|
366
|
-
sendProximity("PROXIMITY_EXIT", key, 0.0
|
|
387
|
+
private fun cleanupBeacon(key: String) {
|
|
388
|
+
sendProximity("PROXIMITY_EXIT", key, 0.0)
|
|
367
389
|
activeCampaigns.remove(key)
|
|
368
390
|
lastProximityEventSent.remove(key)
|
|
369
391
|
lastProximityDistance.remove(key)
|
|
@@ -376,12 +398,11 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
376
398
|
}
|
|
377
399
|
|
|
378
400
|
private fun cleanupAllState() {
|
|
379
|
-
val deviceId = getDeviceId()
|
|
380
401
|
// Stagger exit events to avoid firing N simultaneous network requests
|
|
381
402
|
val keys = activeCampaigns.keys.toList()
|
|
382
403
|
keys.forEachIndexed { index, key ->
|
|
383
404
|
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
384
|
-
cleanupBeacon(key
|
|
405
|
+
cleanupBeacon(key)
|
|
385
406
|
}, index * 300L)
|
|
386
407
|
}
|
|
387
408
|
}
|
|
@@ -398,6 +419,9 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
398
419
|
val conn = (URL("$backendURL$endpoint").openConnection() as HttpURLConnection).apply {
|
|
399
420
|
requestMethod = "POST"
|
|
400
421
|
setRequestProperty("Content-Type", "application/json")
|
|
422
|
+
setRequestProperty("X-Device-ID", getDeviceId())
|
|
423
|
+
sdkToken?.let { setRequestProperty("Authorization", "Bearer $it") }
|
|
424
|
+
source?.let { setRequestProperty("X-Source", it) }
|
|
401
425
|
connectTimeout = 10_000
|
|
402
426
|
readTimeout = 10_000
|
|
403
427
|
doOutput = true
|
|
@@ -436,7 +460,7 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
436
460
|
|
|
437
461
|
// ── Campaign fetching ─────────────────────────────────────────────────────
|
|
438
462
|
|
|
439
|
-
private fun fetchCampaign(major: Int, minor: Int
|
|
463
|
+
private fun fetchCampaign(major: Int, minor: Int) {
|
|
440
464
|
val key = beaconKey(major, minor)
|
|
441
465
|
if (activeCampaigns.containsKey(key)) return
|
|
442
466
|
if (fetchInProgress[key] == true) return
|
|
@@ -447,9 +471,8 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
447
471
|
lastCampaignFetchAttempt[key] = System.currentTimeMillis()
|
|
448
472
|
fetchInProgress[key] = true
|
|
449
473
|
|
|
450
|
-
val payload = mutableMapOf<String, Any?>("beacon_id" to key
|
|
474
|
+
val payload = mutableMapOf<String, Any?>("beacon_id" to key)
|
|
451
475
|
identifiedUserId?.let { payload["user_id"] = it }
|
|
452
|
-
source?.let { payload["source"] = it }
|
|
453
476
|
|
|
454
477
|
post("/api/app/campaigns/beacon", payload) { status, body ->
|
|
455
478
|
fetchInProgress[key] = false
|
|
@@ -487,7 +510,6 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
487
510
|
eventType: String,
|
|
488
511
|
key: String,
|
|
489
512
|
distance: Double,
|
|
490
|
-
deviceId: String,
|
|
491
513
|
endpoint: String
|
|
492
514
|
) {
|
|
493
515
|
val isImpression = eventType == "IMPRESSION_HEARTBEAT"
|
|
@@ -509,14 +531,12 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
509
531
|
|
|
510
532
|
val payload = mutableMapOf<String, Any?>(
|
|
511
533
|
"event_type" to eventType,
|
|
512
|
-
"device_id" to deviceId,
|
|
513
534
|
"distance" to distance,
|
|
514
535
|
"screen_id" to campaign.screenId
|
|
515
536
|
)
|
|
516
537
|
campaign.campaignId?.let { payload["campaign_id"] = it }
|
|
517
538
|
campaign.sessionId?.let { payload["session_id"] = it }
|
|
518
539
|
identifiedUserId?.let { payload["user_id"] = it }
|
|
519
|
-
source?.let { payload["source"] = it }
|
|
520
540
|
|
|
521
541
|
post(endpoint, payload) { status, body ->
|
|
522
542
|
if (status in 200..299) {
|
|
@@ -546,11 +566,11 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
|
|
|
546
566
|
}
|
|
547
567
|
}
|
|
548
568
|
|
|
549
|
-
private fun sendProximity(eventType: String, key: String, distance: Double
|
|
550
|
-
sendTracking(eventType, key, distance,
|
|
569
|
+
private fun sendProximity(eventType: String, key: String, distance: Double) =
|
|
570
|
+
sendTracking(eventType, key, distance, "/api/app/impressions/proximity")
|
|
551
571
|
|
|
552
|
-
private fun sendImpression(key: String, distance: Double
|
|
553
|
-
sendTracking("IMPRESSION_HEARTBEAT", key, distance,
|
|
572
|
+
private fun sendImpression(key: String, distance: Double) =
|
|
573
|
+
sendTracking("IMPRESSION_HEARTBEAT", key, distance, "/api/app/impressions/track")
|
|
554
574
|
|
|
555
575
|
// ── Minimal JSON parser (avoids org.json / GSON dependency) ──────────────
|
|
556
576
|
// Only handles flat and one-level-deep objects. For this SDK, that is enough.
|
|
@@ -61,6 +61,8 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
61
61
|
private var distanceCorrectionFactor: Double = 0.5
|
|
62
62
|
/// Company/brand name that identifies the SDK consumer (e.g. "nike").
|
|
63
63
|
private var source: String?
|
|
64
|
+
/// Bearer token used to authorise all API calls. Set during initialize().
|
|
65
|
+
private var sdkToken: String?
|
|
64
66
|
|
|
65
67
|
// ── Session TTL ────────────────────────────────────────────────────────────
|
|
66
68
|
/// Sessions older than this when the app resumes are considered stale (crash / force-quit).
|
|
@@ -196,6 +198,7 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
196
198
|
resolve: @escaping (Any?) -> Void,
|
|
197
199
|
reject: @escaping (String?, String?, Error?) -> Void
|
|
198
200
|
) {
|
|
201
|
+
// Apply config values
|
|
199
202
|
if let dist = config["maxDetectionDistance"] as? Double {
|
|
200
203
|
maxDetectionDistance = dist
|
|
201
204
|
print("⚙️ SpotnySDK: maxDetectionDistance = \(dist)m")
|
|
@@ -208,8 +211,35 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
208
211
|
distanceCorrectionFactor = factor
|
|
209
212
|
print("⚙️ SpotnySDK: distanceCorrectionFactor = \(factor)")
|
|
210
213
|
}
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
guard let token = config["token"] as? String, !token.isEmpty else {
|
|
215
|
+
reject("MISSING_TOKEN", "initialize() requires a token", nil)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
sdkToken = token
|
|
219
|
+
|
|
220
|
+
// Verify the token — device_id and source are sent as headers automatically
|
|
221
|
+
post(endpoint: "/api/app/sdk/verify", payload: [:]) { [weak self] result in
|
|
222
|
+
guard let self = self else { return }
|
|
223
|
+
switch result {
|
|
224
|
+
case .success(let (status, _)):
|
|
225
|
+
if 200...299 ~= status {
|
|
226
|
+
print("✅ SpotnySDK: Initialized and authorized (source: \(self.source ?? "none"))")
|
|
227
|
+
resolve("SDK initialized")
|
|
228
|
+
} else if status == 401 || status == 403 {
|
|
229
|
+
self.sdkToken = nil
|
|
230
|
+
print("❌ SpotnySDK: Token unauthorized (\(status))")
|
|
231
|
+
reject("UNAUTHORIZED", "Invalid or expired SDK token", nil)
|
|
232
|
+
} else {
|
|
233
|
+
self.sdkToken = nil
|
|
234
|
+
print("❌ SpotnySDK: Verification failed (\(status))")
|
|
235
|
+
reject("VERIFY_FAILED", "SDK verification returned status \(status)", nil)
|
|
236
|
+
}
|
|
237
|
+
case .failure(let error):
|
|
238
|
+
self.sdkToken = nil
|
|
239
|
+
print("❌ SpotnySDK: Verification network error — \(error.localizedDescription)")
|
|
240
|
+
reject("VERIFY_ERROR", error.localizedDescription, error)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
213
243
|
}
|
|
214
244
|
|
|
215
245
|
@objc
|
|
@@ -260,8 +290,7 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
260
290
|
reject: @escaping (String?, String?, Error?) -> Void
|
|
261
291
|
) {
|
|
262
292
|
identifiedUserId = userId
|
|
263
|
-
|
|
264
|
-
if let src = source { payload["source"] = src }
|
|
293
|
+
let payload: [String: Any] = ["user_id": userId]
|
|
265
294
|
post(endpoint: "/api/app/users/identify", payload: payload) { result in
|
|
266
295
|
switch result {
|
|
267
296
|
case .success(let (status, _)):
|
|
@@ -444,6 +473,10 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
444
473
|
var req = URLRequest(url: url)
|
|
445
474
|
req.httpMethod = "POST"
|
|
446
475
|
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
476
|
+
if let token = sdkToken { req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") }
|
|
477
|
+
// device_id and source injected on every request — backend can verify without parsing the body
|
|
478
|
+
req.setValue(getDeviceId(), forHTTPHeaderField: "X-Device-ID")
|
|
479
|
+
if let src = source { req.setValue(src, forHTTPHeaderField: "X-Source") }
|
|
447
480
|
req.timeoutInterval = 10.0
|
|
448
481
|
|
|
449
482
|
var bg: UIBackgroundTaskIdentifier = .invalid
|
|
@@ -478,7 +511,7 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
478
511
|
return campaignFetchCooldown * pow(3.0, Double(min(retries, 3)))
|
|
479
512
|
}
|
|
480
513
|
|
|
481
|
-
private func fetchCampaign(major: Int, minor: Int
|
|
514
|
+
private func fetchCampaign(major: Int, minor: Int) {
|
|
482
515
|
let key = beaconKey(major: major, minor: minor)
|
|
483
516
|
guard activeCampaigns[key] == nil else { return }
|
|
484
517
|
guard fetchInProgress[key] != true else { return }
|
|
@@ -491,9 +524,8 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
491
524
|
lastCampaignFetchAttempt[key] = Date()
|
|
492
525
|
fetchInProgress[key] = true
|
|
493
526
|
|
|
494
|
-
var payload: [String: Any] = ["beacon_id": key
|
|
527
|
+
var payload: [String: Any] = ["beacon_id": key]
|
|
495
528
|
if let uid = identifiedUserId { payload["user_id"] = uid }
|
|
496
|
-
if let src = source { payload["source"] = src }
|
|
497
529
|
|
|
498
530
|
post(endpoint: "/api/app/campaigns/beacon", payload: payload) { [weak self] result in
|
|
499
531
|
guard let self = self else { return }
|
|
@@ -541,7 +573,6 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
541
573
|
eventType: String,
|
|
542
574
|
key: String,
|
|
543
575
|
distance: Double,
|
|
544
|
-
deviceId: String,
|
|
545
576
|
endpoint: String
|
|
546
577
|
) {
|
|
547
578
|
let isImpression = eventType == "IMPRESSION_HEARTBEAT"
|
|
@@ -564,14 +595,12 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
564
595
|
|
|
565
596
|
var payload: [String: Any] = [
|
|
566
597
|
"event_type": eventType,
|
|
567
|
-
"device_id": deviceId,
|
|
568
598
|
"distance": distance,
|
|
569
599
|
"screen_id": campaign.screenId
|
|
570
600
|
]
|
|
571
601
|
if let cid = campaign.campaignId { payload["campaign_id"] = cid }
|
|
572
602
|
if let sid = campaign.sessionId { payload["session_id"] = sid }
|
|
573
603
|
if let uid = identifiedUserId { payload["user_id"] = uid }
|
|
574
|
-
if let src = source { payload["source"] = src }
|
|
575
604
|
|
|
576
605
|
post(endpoint: endpoint, payload: payload) { [weak self] result in
|
|
577
606
|
guard let self = self else { return }
|
|
@@ -608,20 +637,20 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
608
637
|
}
|
|
609
638
|
}
|
|
610
639
|
|
|
611
|
-
private func sendProximity(eventType: String, key: String, distance: Double
|
|
640
|
+
private func sendProximity(eventType: String, key: String, distance: Double) {
|
|
612
641
|
sendTracking(eventType: eventType, key: key, distance: distance,
|
|
613
|
-
|
|
642
|
+
endpoint: "/api/app/impressions/proximity")
|
|
614
643
|
}
|
|
615
644
|
|
|
616
|
-
private func sendImpression(key: String, distance: Double
|
|
645
|
+
private func sendImpression(key: String, distance: Double) {
|
|
617
646
|
sendTracking(eventType: "IMPRESSION_HEARTBEAT", key: key, distance: distance,
|
|
618
|
-
|
|
647
|
+
endpoint: "/api/app/impressions/track")
|
|
619
648
|
}
|
|
620
649
|
|
|
621
650
|
// MARK: - State Cleanup
|
|
622
651
|
|
|
623
|
-
private func cleanupBeacon(_ key: String,
|
|
624
|
-
sendProximity(eventType: "PROXIMITY_EXIT", key: key, distance: distance
|
|
652
|
+
private func cleanupBeacon(_ key: String, distance: Double = 0) {
|
|
653
|
+
sendProximity(eventType: "PROXIMITY_EXIT", key: key, distance: distance)
|
|
625
654
|
activeCampaigns.removeValue(forKey: key)
|
|
626
655
|
lastProximityEventSent.removeValue(forKey: key)
|
|
627
656
|
lastProximityDistance.removeValue(forKey: key)
|
|
@@ -635,12 +664,11 @@ public class SpotnyBeaconScanner: NSObject {
|
|
|
635
664
|
}
|
|
636
665
|
|
|
637
666
|
private func cleanupAllProximityState() {
|
|
638
|
-
let deviceId = getDeviceId()
|
|
639
667
|
// Stagger exit events to avoid firing N simultaneous network requests
|
|
640
668
|
let keys = Array(activeCampaigns.keys)
|
|
641
669
|
for (index, key) in keys.enumerated() {
|
|
642
670
|
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.3) { [weak self] in
|
|
643
|
-
self?.cleanupBeacon(key
|
|
671
|
+
self?.cleanupBeacon(key)
|
|
644
672
|
}
|
|
645
673
|
}
|
|
646
674
|
}
|
|
@@ -669,7 +697,6 @@ extension SpotnyBeaconScanner: KTKBeaconManagerDelegate {
|
|
|
669
697
|
didRangeBeacons beacons: [CLBeacon],
|
|
670
698
|
in region: KTKBeaconRegion
|
|
671
699
|
) {
|
|
672
|
-
let deviceId = getDeviceId()
|
|
673
700
|
let now = Date()
|
|
674
701
|
|
|
675
702
|
// Refresh session heartbeat every 60 s to prevent TTL expiry on a live session
|
|
@@ -724,13 +751,13 @@ extension SpotnyBeaconScanner: KTKBeaconManagerDelegate {
|
|
|
724
751
|
let isFirst = lastProximityEventSent[key] == nil
|
|
725
752
|
|
|
726
753
|
if isFirst {
|
|
727
|
-
sendProximity(eventType: "NEARBY", key: key, distance: distance
|
|
754
|
+
sendProximity(eventType: "NEARBY", key: key, distance: distance)
|
|
728
755
|
lastProximityDistance[key] = distance
|
|
729
756
|
lastProximityEventSent[key] = now
|
|
730
757
|
} else if distance >= 1.0,
|
|
731
758
|
let lastDist = lastProximityDistance[key],
|
|
732
759
|
abs(distance - lastDist) >= proximityDistanceThreshold {
|
|
733
|
-
sendProximity(eventType: "NEARBY", key: key, distance: distance
|
|
760
|
+
sendProximity(eventType: "NEARBY", key: key, distance: distance)
|
|
734
761
|
lastProximityDistance[key] = distance
|
|
735
762
|
lastProximityEventSent[key] = now
|
|
736
763
|
}
|
|
@@ -739,16 +766,16 @@ extension SpotnyBeaconScanner: KTKBeaconManagerDelegate {
|
|
|
739
766
|
if let _ = campaign.campaignId, !campaign.inQueue, distance <= impressionDistance {
|
|
740
767
|
if let last = lastImpressionEventSent[key] {
|
|
741
768
|
if now.timeIntervalSince(last) >= impressionEventInterval {
|
|
742
|
-
sendImpression(key: key, distance: distance
|
|
769
|
+
sendImpression(key: key, distance: distance)
|
|
743
770
|
lastImpressionEventSent[key] = now
|
|
744
771
|
}
|
|
745
772
|
} else {
|
|
746
|
-
sendImpression(key: key, distance: distance
|
|
773
|
+
sendImpression(key: key, distance: distance)
|
|
747
774
|
lastImpressionEventSent[key] = now
|
|
748
775
|
}
|
|
749
776
|
}
|
|
750
777
|
} else {
|
|
751
|
-
fetchCampaign(major: major, minor: minor
|
|
778
|
+
fetchCampaign(major: major, minor: minor)
|
|
752
779
|
}
|
|
753
780
|
}
|
|
754
781
|
}
|
|
@@ -773,7 +800,7 @@ extension SpotnyBeaconScanner: KTKBeaconManagerDelegate {
|
|
|
773
800
|
lastCampaignFetchAttempt.removeValue(forKey: key)
|
|
774
801
|
fetchRetryCount.removeValue(forKey: key)
|
|
775
802
|
if activeCampaigns[key] == nil {
|
|
776
|
-
fetchCampaign(major: major, minor: minor
|
|
803
|
+
fetchCampaign(major: major, minor: minor)
|
|
777
804
|
}
|
|
778
805
|
}
|
|
779
806
|
}
|
|
@@ -792,7 +819,7 @@ extension SpotnyBeaconScanner: KTKBeaconManagerDelegate {
|
|
|
792
819
|
|
|
793
820
|
let parts = region.identifier.components(separatedBy: "_")
|
|
794
821
|
if parts.count == 3, let major = Int(parts[1]), let minor = Int(parts[2]) {
|
|
795
|
-
cleanupBeacon(beaconKey(major: major, minor: minor)
|
|
822
|
+
cleanupBeacon(beaconKey(major: major, minor: minor))
|
|
796
823
|
}
|
|
797
824
|
|
|
798
825
|
beaconManager.stopRangingBeacons(in: region)
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["NativeEventEmitter","NativeModules","NativeSpotnySdk","SpotnyEvents","ON_BEACONS_RANGED","ON_BEACON_REGION_EVENT","eventEmitter","SpotnySdk","startScanner","stopScanner","isScanning","initialize","config","identify","userId","requestNotificationPermissions","getDebugLogs","clearDebugLogs","setDebounceInterval","interval","clearDebounceCache","getDebounceStatus","addBeaconsRangedListener","callback","addListener","addBeaconRegionListener"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,kBAAkB,EAAEC,aAAa,QAAQ,cAAc;AAChE,OAAOC,eAAe,MAAM,sBAAmB;;AAE/C;AACA,OAAO,MAAMC,YAAY,GAAG;EAC1BC,iBAAiB,EAAE,iBAAiB;EACpCC,sBAAsB,EAAE;AAC1B,CAAU;;AAEV;;
|
|
1
|
+
{"version":3,"names":["NativeEventEmitter","NativeModules","NativeSpotnySdk","SpotnyEvents","ON_BEACONS_RANGED","ON_BEACON_REGION_EVENT","eventEmitter","SpotnySdk","startScanner","stopScanner","isScanning","initialize","config","identify","userId","requestNotificationPermissions","getDebugLogs","clearDebugLogs","setDebounceInterval","interval","clearDebounceCache","getDebounceStatus","addBeaconsRangedListener","callback","addListener","addBeaconRegionListener"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,kBAAkB,EAAEC,aAAa,QAAQ,cAAc;AAChE,OAAOC,eAAe,MAAM,sBAAmB;;AAE/C;AACA,OAAO,MAAMC,YAAY,GAAG;EAC1BC,iBAAiB,EAAE,iBAAiB;EACpCC,sBAAsB,EAAE;AAC1B,CAAU;;AAEV;;AA0CA;AACA,MAAMC,YAAY,GAAG,IAAIN,kBAAkB,CAACC,aAAa,CAACM,SAAS,CAAC;;AAEpE;;AAEA;AACA,OAAO,SAASC,YAAYA,CAAA,EAAoB;EAC9C,OAAON,eAAe,CAACM,YAAY,CAAC,CAAC;AACvC;;AAEA;AACA,OAAO,SAASC,WAAWA,CAAA,EAAoB;EAC7C,OAAOP,eAAe,CAACO,WAAW,CAAC,CAAC;AACtC;;AAEA;AACA,OAAO,SAASC,UAAUA,CAAA,EAAqB;EAC7C,OAAOR,eAAe,CAACQ,UAAU,CAAC,CAAC;AACrC;;AAEA;;AAEA;AACA;AACA;AACA,OAAO,SAASC,UAAUA,CAACC,MAAuB,EAAmB;EACnE,OAAOV,eAAe,CAACS,UAAU,CAACC,MAAgB,CAAC;AACrD;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,QAAQA,CAACC,MAAc,EAAmB;EACxD,OAAOZ,eAAe,CAACW,QAAQ,CAACC,MAAM,CAAC;AACzC;;AAEA;;AAEA;AACA,OAAO,SAASC,8BAA8BA,CAAA,EAAoB;EAChE,OAAOb,eAAe,CAACa,8BAA8B,CAAC,CAAC;AACzD;;AAEA;;AAEA;AACA,OAAO,SAASC,YAAYA,CAAA,EAAoB;EAC9C,OAAOd,eAAe,CAACc,YAAY,CAAC,CAAC;AACvC;;AAEA;AACA,OAAO,SAASC,cAAcA,CAAA,EAAoB;EAChD,OAAOf,eAAe,CAACe,cAAc,CAAC,CAAC;AACzC;;AAEA;;AAEA;AACA,OAAO,SAASC,mBAAmBA,CAACC,QAAgB,EAAmB;EACrE,OAAOjB,eAAe,CAACgB,mBAAmB,CAACC,QAAQ,CAAC;AACtD;;AAEA;AACA,OAAO,SAASC,kBAAkBA,CAAA,EAAoB;EACpD,OAAOlB,eAAe,CAACkB,kBAAkB,CAAC,CAAC;AAC7C;;AAEA;AACA,OAAO,SAASC,iBAAiBA,CAAA,EAAoB;EACnD,OAAOnB,eAAe,CAACmB,iBAAiB,CAAC,CAAC;AAC5C;;AAEA;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASC,wBAAwBA,CACtCC,QAA4C,EAC5C;EACA,OAAOjB,YAAY,CAACkB,WAAW,CAC7BrB,YAAY,CAACC,iBAAiB,EAC9BmB,QACF,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASE,uBAAuBA,CACrCF,QAA4C,EAC5C;EACA,OAAOjB,YAAY,CAACkB,WAAW,CAC7BrB,YAAY,CAACE,sBAAsB,EACnCkB,QACF,CAAC;AACH","ignoreList":[]}
|
|
@@ -23,10 +23,15 @@ export type BeaconRegionEvent = {
|
|
|
23
23
|
state?: 'inside' | 'outside' | 'unknown';
|
|
24
24
|
};
|
|
25
25
|
export type SpotnySdkConfig = {
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* SDK token issued by Spotny for your app.
|
|
28
|
+
* Verified against the backend during initialize(). Required.
|
|
29
|
+
*/
|
|
30
|
+
token: string;
|
|
28
31
|
/** Identifier for your brand or app (e.g. 'nike'). */
|
|
29
32
|
source?: string;
|
|
33
|
+
/** Maximum BLE detection distance in metres (default: 8.0) */
|
|
34
|
+
maxDetectionDistance?: number;
|
|
30
35
|
/**
|
|
31
36
|
* Multiplier applied to raw RSSI-derived distance to compensate for device
|
|
32
37
|
* TX power variance (default: 0.5, tuned for Kontakt.io -12 dBm beacons).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,YAAY;;;CAGf,CAAC;AAIX,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,YAAY,CAAC;IACvC,KAAK,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,YAAY;;;CAGf,CAAC;AAIX,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,YAAY,CAAC;IACvC,KAAK,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC,CAAC;AAOF,6BAA6B;AAC7B,wBAAgB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAE9C;AAED,mDAAmD;AACnD,wBAAgB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAE7C;AAED,uDAAuD;AACvD,wBAAgB,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAE7C;AAID;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAEnE;AAID;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAExD;AAID,gEAAgE;AAChE,wBAAgB,8BAA8B,IAAI,OAAO,CAAC,MAAM,CAAC,CAEhE;AAID,yCAAyC;AACzC,wBAAgB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAE9C;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAEhD;AAID,gEAAgE;AAChE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAErE;AAED,2DAA2D;AAC3D,wBAAgB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAEpD;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAEnD;AAID;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,4CAM7C;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,4CAM7C"}
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -33,10 +33,15 @@ export type BeaconRegionEvent = {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
export type SpotnySdkConfig = {
|
|
36
|
-
/**
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* SDK token issued by Spotny for your app.
|
|
38
|
+
* Verified against the backend during initialize(). Required.
|
|
39
|
+
*/
|
|
40
|
+
token: string;
|
|
38
41
|
/** Identifier for your brand or app (e.g. 'nike'). */
|
|
39
42
|
source?: string;
|
|
43
|
+
/** Maximum BLE detection distance in metres (default: 8.0) */
|
|
44
|
+
maxDetectionDistance?: number;
|
|
40
45
|
/**
|
|
41
46
|
* Multiplier applied to raw RSSI-derived distance to compensate for device
|
|
42
47
|
* TX power variance (default: 0.5, tuned for Kontakt.io -12 dBm beacons).
|