spotny-sdk 0.2.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.

@@ -0,0 +1,565 @@
1
+ package com.spotnysdk
2
+
3
+ import android.content.Context
4
+ import android.os.RemoteException
5
+ import android.util.Log
6
+ import com.facebook.react.bridge.*
7
+ import com.facebook.react.modules.core.DeviceEventManagerModule
8
+ import org.altbeacon.beacon.*
9
+ import java.io.File
10
+ import java.net.HttpURLConnection
11
+ import java.net.URL
12
+ import java.text.SimpleDateFormat
13
+ import java.util.*
14
+ import java.util.concurrent.Executors
15
+ import kotlin.math.abs
16
+
17
+ // ── Kontakt.io iBeacon layout ─────────────────────────────────────────────────
18
+ private const val IBEACON_LAYOUT = "m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24"
19
+ private const val BEACON_UUID = "f7826da6-4fa2-4e98-8024-bc5b71e0893e"
20
+ private const val TAG = "SpotnySDK"
21
+
22
+ // ── Campaign data ─────────────────────────────────────────────────────────────
23
+ private data class CampaignData(
24
+ val campaignId: Int?,
25
+ val screenId: Int,
26
+ val sessionId: String?,
27
+ val inQueue: Boolean,
28
+ val major: Int,
29
+ val minor: Int
30
+ )
31
+
32
+ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
33
+ NativeSpotnySdkSpec(reactContext) {
34
+
35
+ // ── Managers ──────────────────────────────────────────────────────────────
36
+ private val beaconManager: BeaconManager = BeaconManager.getInstanceForApplication(reactContext)
37
+ private val ioExecutor = Executors.newCachedThreadPool()
38
+
39
+ // ── Session state ─────────────────────────────────────────────────────────
40
+ private var currentUserUUID: String? = null
41
+ private var userId: Int? = null
42
+ @Volatile private var scanning = false
43
+
44
+ // ── Configuration ─────────────────────────────────────────────────────────
45
+ private var backendURL = "https://api.spotny.app"
46
+ private var maxDetectionDistance = 8.0 // metres
47
+
48
+ // ── Timing constants ──────────────────────────────────────────────────────
49
+ private var campaignFetchCooldown = 5_000L // ms
50
+ private var proximityDistanceThreshold = 0.75
51
+ private var impressionEventInterval = 10_000L // ms
52
+ private val impressionDistance = 2.0
53
+ private var debounceInterval = 5_000L // ms
54
+
55
+ // ── Per-beacon state ──────────────────────────────────────────────────────
56
+ private val activeCampaigns = mutableMapOf<String, CampaignData>()
57
+ private val lastProximityEventSent = mutableMapOf<String, Long>()
58
+ private val lastProximityDistance = mutableMapOf<String, Double>()
59
+ private val lastImpressionEventSent = mutableMapOf<String, Long>()
60
+ private val lastCampaignFetchAttempt = mutableMapOf<String, Long>()
61
+ private val fetchInProgress = mutableMapOf<String, Boolean>()
62
+ private val proximityEventInProgress = mutableMapOf<String, Boolean>()
63
+ private val impressionEventInProgress = mutableMapOf<String, Boolean>()
64
+
65
+ // ── Module registration ───────────────────────────────────────────────────
66
+
67
+ override fun getName() = NAME
68
+
69
+ override fun initialize() {
70
+ super.initialize()
71
+ setupBeaconManager()
72
+ resumeStoredSession()
73
+ }
74
+
75
+ // ── Setup ─────────────────────────────────────────────────────────────────
76
+
77
+ private fun setupBeaconManager() {
78
+ BeaconManager.setDebug(false)
79
+ beaconManager.beaconParsers.add(BeaconParser().setBeaconLayout(IBEACON_LAYOUT))
80
+ beaconManager.foregroundScanPeriod = 1_100L
81
+ beaconManager.foregroundBetweenScanPeriod = 0L
82
+ beaconManager.backgroundScanPeriod = 10_000L
83
+ beaconManager.backgroundBetweenScanPeriod = 5_000L
84
+ }
85
+
86
+ private fun resumeStoredSession() {
87
+ val prefs = reactContext.getSharedPreferences("SpotnySDK", Context.MODE_PRIVATE)
88
+ val stored = prefs.getString("userUUID", null) ?: return
89
+ currentUserUUID = stored
90
+ val uid = prefs.getInt("userId", -1).takeIf { it != -1 }
91
+ userId = uid
92
+ Log.d(TAG, "Resuming session for UUID: $stored")
93
+ startBeaconScanning()
94
+ scanning = true
95
+ }
96
+
97
+ // ── Turbo Module methods ──────────────────────────────────────────────────
98
+
99
+ override fun startScanner(userUUID: String, userId: Double?, promise: Promise) {
100
+ if (scanning) { promise.resolve("Already scanning"); return }
101
+
102
+ currentUserUUID = userUUID
103
+ this.userId = userId?.toInt()
104
+
105
+ val prefs = reactContext.getSharedPreferences("SpotnySDK", Context.MODE_PRIVATE).edit()
106
+ prefs.putString("userUUID", userUUID)
107
+ this.userId?.let { prefs.putInt("userId", it) } ?: prefs.remove("userId")
108
+ prefs.apply()
109
+
110
+ startBeaconScanning()
111
+ scanning = true
112
+ Log.d(TAG, "Started scanning for $userUUID")
113
+ promise.resolve("Scanning started")
114
+ }
115
+
116
+ override fun stopScanner(promise: Promise) {
117
+ try {
118
+ beaconManager.removeAllMonitorNotifiers()
119
+ beaconManager.removeAllRangeNotifiers()
120
+ beaconManager.stopRangingBeaconsInRegion(Region("SpotnySDK_General", null, null, null))
121
+ beaconManager.stopMonitoringBeaconsInRegion(Region("SpotnySDK_General", null, null, null))
122
+ } catch (_: RemoteException) {}
123
+
124
+ cleanupAllState()
125
+
126
+ val prefs = reactContext.getSharedPreferences("SpotnySDK", Context.MODE_PRIVATE).edit()
127
+ prefs.remove("userUUID"); prefs.remove("userId"); prefs.apply()
128
+
129
+ scanning = false; currentUserUUID = null; userId = null
130
+ Log.d(TAG, "Stopped scanning")
131
+ promise.resolve("Scanning stopped")
132
+ }
133
+
134
+ override fun isScanning(promise: Promise) = promise.resolve(scanning)
135
+
136
+ override fun configure(config: ReadableMap?, promise: Promise) {
137
+ config?.getString("backendURL")?.let { backendURL = it; Log.d(TAG, "backendURL = $it") }
138
+ config?.getDouble("maxDetectionDistance").takeIf { config?.hasKey("maxDetectionDistance") == true }
139
+ ?.let { maxDetectionDistance = it; Log.d(TAG, "maxDetectionDistance = $it m") }
140
+ promise.resolve("Configuration updated")
141
+ }
142
+
143
+ override fun requestNotificationPermissions(promise: Promise) {
144
+ // On Android, notification permission is handled at the app level (API 33+).
145
+ // We just resolve — the app should request POST_NOTIFICATIONS separately.
146
+ promise.resolve("handled_by_app")
147
+ }
148
+
149
+ override fun getDebugLogs(promise: Promise) {
150
+ val file = logFile()
151
+ promise.resolve(if (file.exists()) file.readText() else "No logs found")
152
+ }
153
+
154
+ override fun clearDebugLogs(promise: Promise) {
155
+ logFile().takeIf { it.exists() }?.delete()
156
+ promise.resolve("Logs cleared")
157
+ }
158
+
159
+ override fun setDebounceInterval(interval: Double, promise: Promise) {
160
+ debounceInterval = interval.toLong() * 1000L
161
+ promise.resolve("Debounce interval set to ${interval}s")
162
+ }
163
+
164
+ override fun clearDebounceCache(promise: Promise) {
165
+ lastCampaignFetchAttempt.clear(); fetchInProgress.clear()
166
+ promise.resolve("Debounce cache cleared")
167
+ }
168
+
169
+ override fun getDebounceStatus(promise: Promise) {
170
+ val map = WritableNativeMap()
171
+ for ((key, _) in activeCampaigns) {
172
+ val entry = WritableNativeMap()
173
+ lastCampaignFetchAttempt[key]?.let { entry.putDouble("lastFetchAttempt", it.toDouble()) }
174
+ fetchInProgress[key]?.let { entry.putBoolean("fetchInProgress", it) }
175
+ lastProximityEventSent[key]?.let { entry.putDouble("lastProximityEvent", it.toDouble()) }
176
+ lastImpressionEventSent[key]?.let { entry.putDouble("lastImpressionEvent", it.toDouble()) }
177
+ map.putMap(key, entry)
178
+ }
179
+ promise.resolve(map)
180
+ }
181
+
182
+ // Required by NativeEventEmitter contract in new-arch
183
+ override fun addListener(eventName: String) {}
184
+ override fun removeListeners(count: Double) {}
185
+
186
+ // ── Beacon scanning ──────────────────────────────────────────────────────
187
+
188
+ private fun startBeaconScanning() {
189
+ val region = Region("SpotnySDK_General", Identifier.parse(BEACON_UUID), null, null)
190
+
191
+ beaconManager.addRangeNotifier(rangeNotifier)
192
+ beaconManager.addMonitorNotifier(monitorNotifier)
193
+
194
+ beaconManager.startRangingBeaconsInRegion(region)
195
+ beaconManager.startMonitoringBeaconsInRegion(region)
196
+
197
+ Log.d(TAG, "Scanning for UUID $BEACON_UUID")
198
+ }
199
+
200
+ // ── Range notifier ────────────────────────────────────────────────────────
201
+
202
+ private val rangeNotifier = RangeNotifier { beacons, region ->
203
+ val deviceId = getDeviceId()
204
+ val now = System.currentTimeMillis()
205
+
206
+ // JS event payload
207
+ val jsBeacons = WritableNativeArray()
208
+ for (beacon in beacons) {
209
+ val raw = beacon.distance
210
+ val adjusted = raw * 0.5
211
+ if (adjusted <= 0 || adjusted > maxDetectionDistance) continue
212
+
213
+ val b = WritableNativeMap().apply {
214
+ putString("uuid", beacon.id1?.toString() ?: BEACON_UUID)
215
+ putInt("major", beacon.id2?.toInt() ?: 0)
216
+ putInt("minor", beacon.id3?.toInt() ?: 0)
217
+ putDouble("distance", adjusted)
218
+ putInt("rssi", beacon.rssi)
219
+ putString("proximity", proximityLabel(adjusted))
220
+ }
221
+ jsBeacons.pushMap(b)
222
+ }
223
+ if (jsBeacons.size() > 0) {
224
+ val payload = WritableNativeMap().apply {
225
+ putArray("beacons", jsBeacons)
226
+ putString("region", region.uniqueId)
227
+ }
228
+ sendEvent("onBeaconsRanged", payload)
229
+ }
230
+
231
+ // Per-beacon logic
232
+ for (beacon in beacons) {
233
+ val major = beacon.id2?.toInt() ?: continue
234
+ val minor = beacon.id3?.toInt() ?: continue
235
+ val key = beaconKey(major, minor)
236
+ val distance = beacon.distance * 0.5
237
+
238
+ if (distance <= 0 || distance > maxDetectionDistance) continue
239
+
240
+ if (activeCampaigns.containsKey(key)) {
241
+ val isFirst = !lastProximityEventSent.containsKey(key)
242
+ if (isFirst) {
243
+ sendProximity("NEARBY", key, distance, deviceId)
244
+ lastProximityDistance[key] = distance
245
+ lastProximityEventSent[key] = now
246
+ } else if (distance >= 1.0) {
247
+ val lastDist = lastProximityDistance[key] ?: 0.0
248
+ if (abs(distance - lastDist) >= proximityDistanceThreshold) {
249
+ sendProximity("NEARBY", key, distance, deviceId)
250
+ lastProximityDistance[key] = distance
251
+ lastProximityEventSent[key] = now
252
+ }
253
+ }
254
+ val campaign = activeCampaigns[key]
255
+ if (campaign?.campaignId != null && campaign.inQueue == false && distance <= impressionDistance) {
256
+ val lastImp = lastImpressionEventSent[key]
257
+ if (lastImp == null || now - lastImp >= impressionEventInterval) {
258
+ sendImpression(key, distance, deviceId)
259
+ lastImpressionEventSent[key] = now
260
+ }
261
+ }
262
+ } else {
263
+ fetchCampaign(major, minor, deviceId)
264
+ }
265
+ }
266
+ }
267
+
268
+ // ── Monitor notifier ──────────────────────────────────────────────────────
269
+
270
+ private val monitorNotifier = object : MonitorNotifier {
271
+ override fun didEnterRegion(region: Region) {
272
+ Log.d(TAG, "Entered region: ${region.uniqueId}")
273
+ val payload = WritableNativeMap().apply {
274
+ putString("region", region.uniqueId)
275
+ putString("event", "enter")
276
+ }
277
+ sendEvent("onBeaconRegionEvent", payload)
278
+ try { beaconManager.startRangingBeaconsInRegion(region) } catch (_: RemoteException) {}
279
+ }
280
+
281
+ override fun didExitRegion(region: Region) {
282
+ Log.d(TAG, "Exited region: ${region.uniqueId}")
283
+ val payload = WritableNativeMap().apply {
284
+ putString("region", region.uniqueId)
285
+ putString("event", "exit")
286
+ }
287
+ sendEvent("onBeaconRegionEvent", payload)
288
+ val deviceId = getDeviceId()
289
+ for (key in activeCampaigns.keys.toList()) {
290
+ cleanupBeacon(key, deviceId)
291
+ }
292
+ try { beaconManager.stopRangingBeaconsInRegion(region) } catch (_: RemoteException) {}
293
+ }
294
+
295
+ override fun didDetermineStateForRegion(state: Int, region: Region) {
296
+ val label = if (state == MonitorNotifier.INSIDE) "inside" else "outside"
297
+ val payload = WritableNativeMap().apply {
298
+ putString("region", region.uniqueId)
299
+ putString("event", "determined")
300
+ putString("state", label)
301
+ }
302
+ sendEvent("onBeaconRegionEvent", payload)
303
+ }
304
+ }
305
+
306
+ // ── Helpers ───────────────────────────────────────────────────────────────
307
+
308
+ private fun beaconKey(major: Int, minor: Int) = "${major}_${minor}"
309
+
310
+ private fun proximityLabel(distance: Double) = when {
311
+ distance < 0 -> "unknown"
312
+ distance < 0.5 -> "immediate"
313
+ distance < 3.0 -> "near"
314
+ else -> "far"
315
+ }
316
+
317
+ private fun getDeviceId(): String {
318
+ val prefs = reactContext.getSharedPreferences("SpotnySDK", Context.MODE_PRIVATE)
319
+ return prefs.getString("deviceId", null) ?: run {
320
+ val id = UUID.randomUUID().toString()
321
+ prefs.edit().putString("deviceId", id).apply()
322
+ id
323
+ }
324
+ }
325
+
326
+ private fun sendEvent(name: String, payload: WritableMap) {
327
+ reactContext
328
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
329
+ .emit(name, payload)
330
+ }
331
+
332
+ private fun logFile() = File(reactContext.filesDir, "spotny_beacon_debug.log")
333
+
334
+ private fun logToFile(message: String) {
335
+ ioExecutor.execute {
336
+ try {
337
+ val ts = SimpleDateFormat("yy-MM-dd HH:mm:ss", Locale.US).format(Date())
338
+ logFile().appendText("[$ts] $message\n")
339
+ } catch (_: Exception) {}
340
+ }
341
+ }
342
+
343
+ // ── State cleanup ─────────────────────────────────────────────────────────
344
+
345
+ private fun cleanupBeacon(key: String, deviceId: String) {
346
+ sendProximity("PROXIMITY_EXIT", key, 0.0, deviceId)
347
+ activeCampaigns.remove(key)
348
+ lastProximityEventSent.remove(key)
349
+ lastProximityDistance.remove(key)
350
+ lastImpressionEventSent.remove(key)
351
+ lastCampaignFetchAttempt.remove(key)
352
+ fetchInProgress.remove(key)
353
+ proximityEventInProgress.remove(key)
354
+ impressionEventInProgress.remove(key)
355
+ Log.d(TAG, "Cleaned up state for beacon $key")
356
+ }
357
+
358
+ private fun cleanupAllState() {
359
+ val deviceId = getDeviceId()
360
+ activeCampaigns.keys.toList().forEach { cleanupBeacon(it, deviceId) }
361
+ }
362
+
363
+ // ── Backend API ───────────────────────────────────────────────────────────
364
+
365
+ private fun post(
366
+ endpoint: String,
367
+ payload: Map<String, Any?>,
368
+ completion: (Int, String) -> Unit
369
+ ) {
370
+ ioExecutor.execute {
371
+ try {
372
+ val conn = (URL("$backendURL$endpoint").openConnection() as HttpURLConnection).apply {
373
+ requestMethod = "POST"
374
+ setRequestProperty("Content-Type", "application/json")
375
+ connectTimeout = 10_000
376
+ readTimeout = 10_000
377
+ doOutput = true
378
+ }
379
+ val body = buildJsonString(payload)
380
+ conn.outputStream.use { it.write(body.toByteArray()) }
381
+ val status = conn.responseCode
382
+ val response = try { conn.inputStream.bufferedReader().readText() }
383
+ catch (_: Exception) { conn.errorStream?.bufferedReader()?.readText() ?: "" }
384
+ conn.disconnect()
385
+ reactContext.runOnUiQueueThread { completion(status, response) }
386
+ } catch (e: Exception) {
387
+ Log.e(TAG, "POST $endpoint error: ${e.message}")
388
+ reactContext.runOnUiQueueThread { completion(-1, "") }
389
+ }
390
+ }
391
+ }
392
+
393
+ /** Minimal JSON serialiser (avoids adding a full JSON library dependency). */
394
+ private fun buildJsonString(map: Map<String, Any?>): String {
395
+ val sb = StringBuilder("{")
396
+ map.entries.forEachIndexed { i, (k, v) ->
397
+ if (i > 0) sb.append(",")
398
+ sb.append("\"$k\":")
399
+ when (v) {
400
+ null -> sb.append("null")
401
+ is String -> sb.append("\"${v.replace("\"", "\\\"")}\"")
402
+ is Boolean -> sb.append(v)
403
+ is Number -> sb.append(v)
404
+ else -> sb.append("\"$v\"")
405
+ }
406
+ }
407
+ sb.append("}")
408
+ return sb.toString()
409
+ }
410
+
411
+ // ── Campaign fetching ─────────────────────────────────────────────────────
412
+
413
+ private fun fetchCampaign(major: Int, minor: Int, deviceId: String) {
414
+ val key = beaconKey(major, minor)
415
+ if (activeCampaigns.containsKey(key)) return
416
+ if (fetchInProgress[key] == true) return
417
+
418
+ val last = lastCampaignFetchAttempt[key]
419
+ if (last != null && System.currentTimeMillis() - last < campaignFetchCooldown) return
420
+
421
+ lastCampaignFetchAttempt[key] = System.currentTimeMillis()
422
+ fetchInProgress[key] = true
423
+
424
+ val payload = mutableMapOf<String, Any?>("beacon_id" to key, "device_id" to deviceId)
425
+ userId?.let { payload["user_id"] = it }
426
+
427
+ post("/api/app/campaigns/beacon", payload) { status, body ->
428
+ fetchInProgress[key] = false
429
+ if (status != 200) {
430
+ Log.w(TAG, "Campaign fetch status $status for beacon $key")
431
+ return@post
432
+ }
433
+ try {
434
+ val json = parseJsonObject(body) ?: return@post
435
+ val dataObj = json["data"] as? Map<*, *> ?: return@post
436
+ val screen = dataObj["screen"] as? Map<*, *> ?: return@post
437
+ val screenId = (screen["id"] as? Number)?.toInt() ?: return@post
438
+
439
+ var campaignId: Int? = null
440
+ var inQueue = false
441
+ val campaignObj = dataObj["campaign"] as? Map<*, *>
442
+ if (campaignObj != null) {
443
+ campaignId = (campaignObj["id"] as? Number)?.toInt()
444
+ inQueue = campaignObj["inQueue"] as? Boolean ?: false
445
+ }
446
+
447
+ activeCampaigns[key] = CampaignData(
448
+ campaignId = campaignId, screenId = screenId,
449
+ sessionId = null, inQueue = inQueue, major = major, minor = minor)
450
+ Log.d(TAG, "Campaign loaded for beacon $key — screenId=$screenId")
451
+ } catch (e: Exception) {
452
+ Log.e(TAG, "Campaign JSON parse error for $key: ${e.message}")
453
+ }
454
+ }
455
+ }
456
+
457
+ // ── Tracking events ───────────────────────────────────────────────────────
458
+
459
+ private fun sendTracking(
460
+ eventType: String,
461
+ key: String,
462
+ distance: Double,
463
+ deviceId: String,
464
+ endpoint: String
465
+ ) {
466
+ val isImpression = eventType == "IMPRESSION_HEARTBEAT"
467
+ if (isImpression && impressionEventInProgress[key] == true) return
468
+ if (!isImpression && proximityEventInProgress[key] == true) return
469
+
470
+ if (isImpression) impressionEventInProgress[key] = true
471
+ else proximityEventInProgress[key] = true
472
+
473
+ val campaign = activeCampaigns[key]
474
+ if (campaign == null) {
475
+ if (isImpression) impressionEventInProgress[key] = false
476
+ else proximityEventInProgress[key] = false
477
+ return
478
+ }
479
+ if (isImpression && (campaign.campaignId == null || campaign.inQueue)) {
480
+ impressionEventInProgress[key] = false; return
481
+ }
482
+
483
+ val payload = mutableMapOf<String, Any?>(
484
+ "event_type" to eventType,
485
+ "device_id" to deviceId,
486
+ "distance" to distance,
487
+ "screen_id" to campaign.screenId
488
+ )
489
+ campaign.campaignId?.let { payload["campaign_id"] = it }
490
+ campaign.sessionId?.let { payload["session_id"] = it }
491
+ userId?.let { payload["user_id"] = it }
492
+
493
+ post(endpoint, payload) { status, body ->
494
+ if (status in 200..299) {
495
+ Log.d(TAG, "$eventType sent — distance ${"%.2f".format(distance)}m")
496
+ if (!isImpression && campaign.sessionId == null) {
497
+ try {
498
+ val json = parseJsonObject(body)
499
+ val sid = ((json?.get("data") as? Map<*, *>)
500
+ ?.get("event") as? Map<*, *>)
501
+ ?.get("session_id") as? String
502
+ if (sid != null) {
503
+ activeCampaigns[key] = campaign.copy(sessionId = sid)
504
+ Log.d(TAG, "session_id = $sid")
505
+ }
506
+ } catch (_: Exception) {}
507
+ }
508
+ } else if (status == 429) {
509
+ val penalty = System.currentTimeMillis() + 10_000L
510
+ if (isImpression) lastImpressionEventSent[key] = penalty
511
+ else lastProximityEventSent[key] = penalty
512
+ Log.w(TAG, "$eventType rate-limited (429)")
513
+ } else {
514
+ Log.w(TAG, "$eventType failed — status $status")
515
+ }
516
+ if (isImpression) impressionEventInProgress[key] = false
517
+ else proximityEventInProgress[key] = false
518
+ }
519
+ }
520
+
521
+ private fun sendProximity(eventType: String, key: String, distance: Double, deviceId: String) =
522
+ sendTracking(eventType, key, distance, deviceId, "/api/app/impressions/proximity")
523
+
524
+ private fun sendImpression(key: String, distance: Double, deviceId: String) =
525
+ sendTracking("IMPRESSION_HEARTBEAT", key, distance, deviceId, "/api/app/impressions/track")
526
+
527
+ // ── Minimal JSON parser (avoids org.json / GSON dependency) ──────────────
528
+ // Only handles flat and one-level-deep objects. For this SDK, that is enough.
529
+
530
+ @Suppress("UNCHECKED_CAST")
531
+ private fun parseJsonObject(json: String): Map<String, Any?>? {
532
+ return try {
533
+ org.json.JSONObject(json).toMap()
534
+ } catch (_: Exception) { null }
535
+ }
536
+
537
+ private fun org.json.JSONObject.toMap(): Map<String, Any?> {
538
+ val map = mutableMapOf<String, Any?>()
539
+ keys().forEach { key ->
540
+ map[key] = when (val v = get(key)) {
541
+ is org.json.JSONObject -> v.toMap()
542
+ is org.json.JSONArray -> v.toList()
543
+ org.json.JSONObject.NULL -> null
544
+ else -> v
545
+ }
546
+ }
547
+ return map
548
+ }
549
+
550
+ private fun org.json.JSONArray.toList(): List<Any?> {
551
+ return (0 until length()).map { i ->
552
+ when (val v = get(i)) {
553
+ is org.json.JSONObject -> v.toMap()
554
+ is org.json.JSONArray -> v.toList()
555
+ org.json.JSONObject.NULL -> null
556
+ else -> v
557
+ }
558
+ }
559
+ }
560
+
561
+ companion object {
562
+ const val NAME = NativeSpotnySdkSpec.NAME
563
+ }
564
+ }
565
+
@@ -0,0 +1,31 @@
1
+ package com.spotnysdk
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class SpotnySdkPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == SpotnySdkModule.NAME) {
13
+ SpotnySdkModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
20
+ mapOf(
21
+ SpotnySdkModule.NAME to ReactModuleInfo(
22
+ name = SpotnySdkModule.NAME,
23
+ className = SpotnySdkModule.NAME,
24
+ canOverrideExistingModule = false,
25
+ needsEagerInit = false,
26
+ isCxxModule = false,
27
+ isTurboModule = true
28
+ )
29
+ )
30
+ }
31
+ }