spotny-sdk 0.4.1 → 0.5.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.

@@ -37,10 +37,14 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
37
37
  private val ioExecutor = Executors.newCachedThreadPool()
38
38
 
39
39
  // ── Session state ─────────────────────────────────────────────────────────
40
- private var currentUserUUID: String? = null
41
- private var userId: Int? = null
42
40
  @Volatile private var scanning = false
43
41
 
42
+ // ── Identity (persisted in SharedPreferences) ────────────────────────
43
+ private var identifiedUserId: String? = null
44
+ private var profilePhone: String? = null
45
+ private var profileEmail: String? = null
46
+ private var profileName: String? = null
47
+
44
48
  // ── Configuration ─────────────────────────────────────────────────────────
45
49
  // backendURL is fixed — not overridable by consumers.
46
50
  private val backendURL = "https://api.spotny.app"
@@ -90,31 +94,30 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
90
94
 
91
95
  private fun resumeStoredSession() {
92
96
  val prefs = reactContext.getSharedPreferences("SpotnySDK", Context.MODE_PRIVATE)
93
- val stored = prefs.getString("userUUID", null) ?: return
94
- currentUserUUID = stored
95
- val uid = prefs.getInt("userId", -1).takeIf { it != -1 }
96
- userId = uid
97
- Log.d(TAG, "Resuming session for UUID: $stored")
97
+ // Reload identity
98
+ identifiedUserId = prefs.getString("userId", null)
99
+ profilePhone = prefs.getString("profilePhone", null)
100
+ profileEmail = prefs.getString("profileEmail", null)
101
+ profileName = prefs.getString("profileName", null)
102
+ // Resume scanning only if a valid session was previously active
103
+ if (!prefs.getBoolean("sessionActive", false)) return
104
+ Log.d(TAG, "Resuming session (userId: ${identifiedUserId ?: "anonymous"})")
98
105
  startBeaconScanning()
99
106
  scanning = true
100
107
  }
101
108
 
102
109
  // ── Turbo Module methods ──────────────────────────────────────────────────
103
110
 
104
- override fun startScanner(userUUID: String, userId: Double?, promise: Promise) {
111
+ override fun startScanner(promise: Promise) {
105
112
  if (scanning) { promise.resolve("Already scanning"); return }
106
113
 
107
- currentUserUUID = userUUID
108
- this.userId = userId?.toInt()
109
-
110
114
  val prefs = reactContext.getSharedPreferences("SpotnySDK", Context.MODE_PRIVATE).edit()
111
- prefs.putString("userUUID", userUUID)
112
- this.userId?.let { prefs.putInt("userId", it) } ?: prefs.remove("userId")
115
+ prefs.putBoolean("sessionActive", true)
113
116
  prefs.apply()
114
117
 
115
118
  startBeaconScanning()
116
119
  scanning = true
117
- Log.d(TAG, "Started scanning for $userUUID")
120
+ Log.d(TAG, "Started scanning (userId: ${identifiedUserId ?: "anonymous"})")
118
121
  promise.resolve("Scanning started")
119
122
  }
120
123
 
@@ -129,9 +132,9 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
129
132
  cleanupAllState()
130
133
 
131
134
  val prefs = reactContext.getSharedPreferences("SpotnySDK", Context.MODE_PRIVATE).edit()
132
- prefs.remove("userUUID"); prefs.remove("userId"); prefs.apply()
135
+ prefs.remove("sessionActive"); prefs.apply()
133
136
 
134
- scanning = false; currentUserUUID = null; userId = null
137
+ scanning = false
135
138
  Log.d(TAG, "Stopped scanning")
136
139
  promise.resolve("Scanning stopped")
137
140
  }
@@ -174,6 +177,51 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
174
177
  promise.resolve("Debounce cache cleared")
175
178
  }
176
179
 
180
+ // ── Identity ──────────────────────────────────────────────────────────────
181
+
182
+ override fun identify(userId: String, promise: Promise) {
183
+ identifiedUserId = userId
184
+ val prefs = reactContext.getSharedPreferences("SpotnySDK", Context.MODE_PRIVATE).edit()
185
+ prefs.putString("userId", userId); prefs.apply()
186
+ Log.d(TAG, "Identified user $userId")
187
+ promise.resolve("User identified")
188
+ }
189
+
190
+ override fun updateProfile(profile: ReadableMap?, promise: Promise) {
191
+ val prefs = reactContext.getSharedPreferences("SpotnySDK", Context.MODE_PRIVATE).edit()
192
+ profile?.getString("phoneNumber")?.let { profilePhone = it; prefs.putString("profilePhone", it) }
193
+ profile?.getString("email")?.let { profileEmail = it; prefs.putString("profileEmail", it) }
194
+ profile?.getString("name")?.let { profileName = it; prefs.putString("profileName", it) }
195
+ prefs.apply()
196
+
197
+ val payload = mutableMapOf<String, Any?>("device_id" to getDeviceId())
198
+ identifiedUserId?.let { payload["user_id"] = it }
199
+ profilePhone?.let { payload["phone"] = it }
200
+ profileEmail?.let { payload["email"] = it }
201
+ profileName?.let { payload["name"] = it }
202
+ source?.let { payload["source"] = it }
203
+
204
+ post("/api/app/profiles/update", payload) { status, _ ->
205
+ if (status in 200..299) {
206
+ Log.d(TAG, "Profile updated")
207
+ promise.resolve("Profile updated")
208
+ } else {
209
+ Log.w(TAG, "Profile update failed — status $status")
210
+ promise.reject("PROFILE_UPDATE_FAILED", "Server returned status $status")
211
+ }
212
+ }
213
+ }
214
+
215
+ override fun resetIdentity(promise: Promise) {
216
+ identifiedUserId = null; profilePhone = null; profileEmail = null; profileName = null
217
+ val prefs = reactContext.getSharedPreferences("SpotnySDK", Context.MODE_PRIVATE).edit()
218
+ prefs.remove("userId"); prefs.remove("profilePhone")
219
+ prefs.remove("profileEmail"); prefs.remove("profileName")
220
+ prefs.apply()
221
+ Log.d(TAG, "Identity reset")
222
+ promise.resolve("Identity reset")
223
+ }
224
+
177
225
  override fun getDebounceStatus(promise: Promise) {
178
226
  val map = WritableNativeMap()
179
227
  for ((key, _) in activeCampaigns) {
@@ -436,8 +484,8 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
436
484
  fetchInProgress[key] = true
437
485
 
438
486
  val payload = mutableMapOf<String, Any?>("beacon_id" to key, "device_id" to deviceId)
439
- userId?.let { payload["user_id"] = it }
440
- source?.let { payload["source"] = it }
487
+ identifiedUserId?.let { payload["user_id"] = it }
488
+ source?.let { payload["source"] = it }
441
489
 
442
490
  post("/api/app/campaigns/beacon", payload) { status, body ->
443
491
  fetchInProgress[key] = false
@@ -503,8 +551,8 @@ class SpotnySdkModule(private val reactContext: ReactApplicationContext) :
503
551
  )
504
552
  campaign.campaignId?.let { payload["campaign_id"] = it }
505
553
  campaign.sessionId?.let { payload["session_id"] = it }
506
- userId?.let { payload["user_id"] = it }
507
- source?.let { payload["source"] = it }
554
+ identifiedUserId?.let { payload["user_id"] = it }
555
+ source?.let { payload["source"] = it }
508
556
 
509
557
  post(endpoint, payload) { status, body ->
510
558
  if (status in 200..299) {
@@ -41,10 +41,14 @@ public class SpotnyBeaconScanner: NSObject {
41
41
  private var locationManager: CLLocationManager!
42
42
 
43
43
  // ── Session state ─────────────────────────────────────────────────────────
44
- private var currentUserUUID: String?
45
- private var userId: Int?
46
44
  private var scanning: Bool = false
47
45
 
46
+ // ── Identity (persisted in Keychain across sessions) ─────────────────────
47
+ private var identifiedUserId: String?
48
+ private var profilePhone: String?
49
+ private var profileEmail: String?
50
+ private var profileName: String?
51
+
48
52
  // ── Configuration ────────────────────────────────────────────────────────────
49
53
  // backendURL and kontaktAPIKey are fixed — not overridable by consumers.
50
54
  private let backendURL: String = "https://api.spotny.app"
@@ -114,26 +118,27 @@ public class SpotnyBeaconScanner: NSObject {
114
118
  }
115
119
 
116
120
  private func resumeStoredSession() {
117
- guard let stored = UserDefaults.standard.string(forKey: "SpotnySDK_userUUID") else { return }
118
- // Discard sessions older than sessionTTL — stale after a crash / force-quit
119
- if let ts = UserDefaults.standard.object(forKey: "SpotnySDK_sessionTimestamp") as? Double {
120
- let age = Date().timeIntervalSince1970 - ts
121
- if age > sessionTTL {
122
- print("⚠️ SpotnySDK: Stale session (\(Int(age / 3600))h old) — discarding")
123
- clearStoredSession()
124
- return
125
- }
121
+ // Always reload identity from Keychain (persists across sessions)
122
+ identifiedUserId = keychainRead(key: "SpotnySDK_userId")
123
+ profilePhone = keychainRead(key: "SpotnySDK_profilePhone")
124
+ profileEmail = keychainRead(key: "SpotnySDK_profileEmail")
125
+ profileName = keychainRead(key: "SpotnySDK_profileName")
126
+
127
+ // Resume scanning only if a valid session was previously active
128
+ guard let ts = UserDefaults.standard.object(forKey: "SpotnySDK_sessionTimestamp") as? Double else { return }
129
+ let age = Date().timeIntervalSince1970 - ts
130
+ if age > sessionTTL {
131
+ print("⚠️ SpotnySDK: Stale session (\(Int(age / 3600))h old) — discarding")
132
+ clearStoredSession()
133
+ return
126
134
  }
127
- currentUserUUID = stored
128
- userId = UserDefaults.standard.object(forKey: "SpotnySDK_userId") as? Int
129
- print("🔄 SpotnySDK: Resuming session for UUID: \(stored)")
135
+ print("🔄 SpotnySDK: Resuming session (userId: \(identifiedUserId ?? "anonymous"))")
130
136
  startPersistentScanning()
131
137
  scanning = true
132
138
  }
133
139
 
134
140
  private func clearStoredSession() {
135
- UserDefaults.standard.removeObject(forKey: "SpotnySDK_userUUID")
136
- UserDefaults.standard.removeObject(forKey: "SpotnySDK_userId")
141
+ // Only clears the scanning session — identity (Keychain) is intentionally kept
137
142
  UserDefaults.standard.removeObject(forKey: "SpotnySDK_sessionTimestamp")
138
143
  UserDefaults.standard.synchronize()
139
144
  }
@@ -142,9 +147,7 @@ public class SpotnyBeaconScanner: NSObject {
142
147
 
143
148
  @objc
144
149
  public func startScanner(
145
- withUserUUID userUUID: String,
146
- userId: NSNumber?,
147
- resolve: @escaping (Any?) -> Void,
150
+ withResolve resolve: @escaping (Any?) -> Void,
148
151
  reject: @escaping (String?, String?, Error?) -> Void
149
152
  ) {
150
153
  if scanning {
@@ -152,15 +155,6 @@ public class SpotnyBeaconScanner: NSObject {
152
155
  return
153
156
  }
154
157
 
155
- currentUserUUID = userUUID
156
- self.userId = userId?.intValue
157
-
158
- UserDefaults.standard.set(userUUID, forKey: "SpotnySDK_userUUID")
159
- if let uid = userId {
160
- UserDefaults.standard.set(uid.intValue, forKey: "SpotnySDK_userId")
161
- } else {
162
- UserDefaults.standard.removeObject(forKey: "SpotnySDK_userId")
163
- }
164
158
  UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "SpotnySDK_sessionTimestamp")
165
159
  UserDefaults.standard.synchronize()
166
160
 
@@ -171,7 +165,7 @@ public class SpotnyBeaconScanner: NSObject {
171
165
 
172
166
  startPersistentScanning()
173
167
  scanning = true
174
- print("✅ SpotnySDK: Started scanning for \(userUUID)")
168
+ print("✅ SpotnySDK: Started scanning (userId: \(identifiedUserId ?? "anonymous"))")
175
169
  resolve("Scanning started")
176
170
  }
177
171
 
@@ -187,8 +181,6 @@ public class SpotnyBeaconScanner: NSObject {
187
181
  clearStoredSession()
188
182
 
189
183
  scanning = false
190
- currentUserUUID = nil
191
- userId = nil
192
184
 
193
185
  print("⏹️ SpotnySDK: Stopped scanning")
194
186
  resolve("Scanning stopped")
@@ -260,6 +252,80 @@ public class SpotnyBeaconScanner: NSObject {
260
252
  resolve("Debounce cache cleared")
261
253
  }
262
254
 
255
+ // MARK: - Identity
256
+
257
+ @objc
258
+ public func identify(
259
+ _ userId: String,
260
+ resolve: @escaping (Any?) -> Void,
261
+ reject: @escaping (String?, String?, Error?) -> Void
262
+ ) {
263
+ identifiedUserId = userId
264
+ keychainWrite(key: "SpotnySDK_userId", value: userId)
265
+ print("👤 SpotnySDK: Identified user \(userId)")
266
+ resolve("User identified")
267
+ }
268
+
269
+ @objc
270
+ public func updateProfile(
271
+ _ profile: NSDictionary,
272
+ resolve: @escaping (Any?) -> Void,
273
+ reject: @escaping (String?, String?, Error?) -> Void
274
+ ) {
275
+ if let phone = profile["phoneNumber"] as? String {
276
+ profilePhone = phone
277
+ keychainWrite(key: "SpotnySDK_profilePhone", value: phone)
278
+ }
279
+ if let email = profile["email"] as? String {
280
+ profileEmail = email
281
+ keychainWrite(key: "SpotnySDK_profileEmail", value: email)
282
+ }
283
+ if let name = profile["name"] as? String {
284
+ profileName = name
285
+ keychainWrite(key: "SpotnySDK_profileName", value: name)
286
+ }
287
+
288
+ var payload: [String: Any] = ["device_id": getDeviceId()]
289
+ if let uid = identifiedUserId { payload["user_id"] = uid }
290
+ if let ph = profilePhone { payload["phone"] = ph }
291
+ if let em = profileEmail { payload["email"] = em }
292
+ if let nm = profileName { payload["name"] = nm }
293
+ if let src = source { payload["source"] = src }
294
+
295
+ post(endpoint: "/api/app/profiles/update", payload: payload) { result in
296
+ switch result {
297
+ case .success(let (status, _)):
298
+ if 200...299 ~= status {
299
+ print("✅ SpotnySDK: Profile updated")
300
+ resolve("Profile updated")
301
+ } else {
302
+ print("❌ SpotnySDK: Profile update failed — status \(status)")
303
+ reject("PROFILE_UPDATE_FAILED", "Server returned status \(status)", nil)
304
+ }
305
+ case .failure(let error):
306
+ print("❌ SpotnySDK: Profile update error — \(error.localizedDescription)")
307
+ reject("PROFILE_UPDATE_ERROR", error.localizedDescription, error)
308
+ }
309
+ }
310
+ }
311
+
312
+ @objc
313
+ public func resetIdentity(
314
+ withResolve resolve: @escaping (Any?) -> Void,
315
+ reject: @escaping (String?, String?, Error?) -> Void
316
+ ) {
317
+ identifiedUserId = nil
318
+ profilePhone = nil
319
+ profileEmail = nil
320
+ profileName = nil
321
+ keychainDelete(key: "SpotnySDK_userId")
322
+ keychainDelete(key: "SpotnySDK_profilePhone")
323
+ keychainDelete(key: "SpotnySDK_profileEmail")
324
+ keychainDelete(key: "SpotnySDK_profileName")
325
+ print("🔓 SpotnySDK: Identity reset")
326
+ resolve("Identity reset")
327
+ }
328
+
263
329
  @objc
264
330
  public func getDebounceStatus(
265
331
  withResolve resolve: @escaping (Any?) -> Void,
@@ -364,6 +430,15 @@ public class SpotnyBeaconScanner: NSObject {
364
430
  }
365
431
  }
366
432
 
433
+ private func keychainDelete(key: String) {
434
+ let query: [CFString: Any] = [
435
+ kSecClass: kSecClassGenericPassword,
436
+ kSecAttrService: keychainService,
437
+ kSecAttrAccount: key
438
+ ]
439
+ SecItemDelete(query as CFDictionary)
440
+ }
441
+
367
442
  private func getDeviceId() -> String {
368
443
  let kcKey = "SpotnySDK_deviceId"
369
444
  // 1. Keychain — survives uninstall
@@ -464,8 +539,8 @@ public class SpotnyBeaconScanner: NSObject {
464
539
  fetchInProgress[key] = true
465
540
 
466
541
  var payload: [String: Any] = ["beacon_id": key, "device_id": deviceId]
467
- if let uid = userId { payload["user_id"] = uid }
468
- if let src = source { payload["source"] = src }
542
+ if let uid = identifiedUserId { payload["user_id"] = uid }
543
+ if let src = source { payload["source"] = src }
469
544
 
470
545
  post(endpoint: "/api/app/campaigns/beacon", payload: payload) { [weak self] result in
471
546
  guard let self = self else { return }
@@ -540,7 +615,7 @@ public class SpotnyBeaconScanner: NSObject {
540
615
  ]
541
616
  if let cid = campaign.campaignId { payload["campaign_id"] = cid }
542
617
  if let sid = campaign.sessionId { payload["session_id"] = sid }
543
- if let uid = userId { payload["user_id"] = uid }
618
+ if let uid = identifiedUserId { payload["user_id"] = uid }
544
619
  if let src = source { payload["source"] = src }
545
620
 
546
621
  post(endpoint: endpoint, payload: payload) { [weak self] result in
package/ios/SpotnySdk.mm CHANGED
@@ -64,11 +64,9 @@ RCT_EXPORT_MODULE()
64
64
 
65
65
  // ── Method implementations ───────────────────────────────────────────────────
66
66
 
67
- - (void)startScanner:(NSString *)userUUID
68
- userId:(NSNumber *)userId
69
- resolve:(RCTPromiseResolveBlock)resolve
67
+ - (void)startScanner:(RCTPromiseResolveBlock)resolve
70
68
  reject:(RCTPromiseRejectBlock)reject {
71
- [_scanner startScannerWithUserUUID:userUUID userId:userId resolve:resolve reject:reject];
69
+ [_scanner startScannerWithResolve:resolve reject:reject];
72
70
  }
73
71
 
74
72
  - (void)stopScanner:(RCTPromiseResolveBlock)resolve
@@ -118,6 +116,23 @@ RCT_EXPORT_MODULE()
118
116
  [_scanner getDebounceStatusWithResolve:resolve reject:reject];
119
117
  }
120
118
 
119
+ - (void)identify:(NSString *)userId
120
+ resolve:(RCTPromiseResolveBlock)resolve
121
+ reject:(RCTPromiseRejectBlock)reject {
122
+ [_scanner identify:userId resolve:resolve reject:reject];
123
+ }
124
+
125
+ - (void)updateProfile:(NSDictionary *)profile
126
+ resolve:(RCTPromiseResolveBlock)resolve
127
+ reject:(RCTPromiseRejectBlock)reject {
128
+ [_scanner updateProfile:profile resolve:resolve reject:reject];
129
+ }
130
+
131
+ - (void)resetIdentity:(RCTPromiseResolveBlock)resolve
132
+ reject:(RCTPromiseRejectBlock)reject {
133
+ [_scanner resetIdentityWithResolve:resolve reject:reject];
134
+ }
135
+
121
136
  // ── TurboModule JSI bridge ───────────────────────────────────────────────────
122
137
 
123
138
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
@@ -1 +1 @@
1
- {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSpotnySdk.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;AA4BpE,eAAeA,mBAAmB,CAACC,YAAY,CAAO,WAAW,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSpotnySdk.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;AAiCpE,eAAeA,mBAAmB,CAACC,YAAY,CAAO,WAAW,CAAC","ignoreList":[]}
@@ -16,13 +16,9 @@ const eventEmitter = new NativeEventEmitter(NativeModules.SpotnySdk);
16
16
 
17
17
  // ── Core scanning ────────────────────────────────────────────────────────────
18
18
 
19
- /**
20
- * Start beacon scanning.
21
- * @param userUUID A unique string identifying this installation / user session.
22
- * @param userId Optional authenticated user ID.
23
- */
24
- export function startScanner(userUUID, userId) {
25
- return NativeSpotnySdk.startScanner(userUUID, userId ?? null);
19
+ /** Start beacon scanning. Call `identify()` before this for attributed tracking. */
20
+ export function startScanner() {
21
+ return NativeSpotnySdk.startScanner();
26
22
  }
27
23
 
28
24
  /** Stop beacon scanning and clean up all state. */
@@ -35,6 +31,35 @@ export function isScanning() {
35
31
  return NativeSpotnySdk.isScanning();
36
32
  }
37
33
 
34
+ // ── Identity ─────────────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Identify the current user. Pass any opaque string your app uses — user ID,
38
+ * hashed email, hashed phone, etc. Stored in Keychain and sent with all
39
+ * subsequent tracking events.
40
+ *
41
+ * Call before `startScanner()` or at any point during an active session.
42
+ */
43
+ export function identify(userId) {
44
+ return NativeSpotnySdk.identify(userId);
45
+ }
46
+
47
+ /**
48
+ * Update the user's profile on the Spotny backend.
49
+ * Fields are optional — pass only what you have.
50
+ */
51
+ export function updateProfile(profile) {
52
+ return NativeSpotnySdk.updateProfile(profile);
53
+ }
54
+
55
+ /**
56
+ * Clear the stored identity (user ID + profile fields) from Keychain.
57
+ * Call on logout. The device ID is NOT affected.
58
+ */
59
+ export function resetIdentity() {
60
+ return NativeSpotnySdk.resetIdentity();
61
+ }
62
+
38
63
  // ── Configuration ────────────────────────────────────────────────────────────
39
64
 
40
65
  /**
@@ -1 +1 @@
1
- {"version":3,"names":["NativeEventEmitter","NativeModules","NativeSpotnySdk","SpotnyEvents","ON_BEACONS_RANGED","ON_BEACON_REGION_EVENT","eventEmitter","SpotnySdk","startScanner","userUUID","userId","stopScanner","isScanning","configure","config","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;;AAqCA;AACA,MAAMC,YAAY,GAAG,IAAIN,kBAAkB,CAACC,aAAa,CAACM,SAAS,CAAC;;AAEpE;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,YAAYA,CAC1BC,QAAgB,EAChBC,MAAsB,EACL;EACjB,OAAOR,eAAe,CAACM,YAAY,CAACC,QAAQ,EAAEC,MAAM,IAAI,IAAI,CAAC;AAC/D;;AAEA;AACA,OAAO,SAASC,WAAWA,CAAA,EAAoB;EAC7C,OAAOT,eAAe,CAACS,WAAW,CAAC,CAAC;AACtC;;AAEA;AACA,OAAO,SAASC,UAAUA,CAAA,EAAqB;EAC7C,OAAOV,eAAe,CAACU,UAAU,CAAC,CAAC;AACrC;;AAEA;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASC,SAASA,CAACC,MAAuB,EAAmB;EAClE,OAAOZ,eAAe,CAACW,SAAS,CAACC,MAAgB,CAAC;AACpD;;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":[]}
1
+ {"version":3,"names":["NativeEventEmitter","NativeModules","NativeSpotnySdk","SpotnyEvents","ON_BEACONS_RANGED","ON_BEACON_REGION_EVENT","eventEmitter","SpotnySdk","startScanner","stopScanner","isScanning","identify","userId","updateProfile","profile","resetIdentity","configure","config","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;;AA8CA;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;AACA;AACA;AACA;AACA,OAAO,SAASC,QAAQA,CAACC,MAAc,EAAmB;EACxD,OAAOV,eAAe,CAACS,QAAQ,CAACC,MAAM,CAAC;AACzC;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASC,aAAaA,CAACC,OAAsB,EAAmB;EACrE,OAAOZ,eAAe,CAACW,aAAa,CAACC,OAAiB,CAAC;AACzD;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASC,aAAaA,CAAA,EAAoB;EAC/C,OAAOb,eAAe,CAACa,aAAa,CAAC,CAAC;AACxC;;AAEA;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASC,SAASA,CAACC,MAAuB,EAAmB;EAClE,OAAOf,eAAe,CAACc,SAAS,CAACC,MAAgB,CAAC;AACpD;;AAEA;;AAEA;AACA,OAAO,SAASC,8BAA8BA,CAAA,EAAoB;EAChE,OAAOhB,eAAe,CAACgB,8BAA8B,CAAC,CAAC;AACzD;;AAEA;;AAEA;AACA,OAAO,SAASC,YAAYA,CAAA,EAAoB;EAC9C,OAAOjB,eAAe,CAACiB,YAAY,CAAC,CAAC;AACvC;;AAEA;AACA,OAAO,SAASC,cAAcA,CAAA,EAAoB;EAChD,OAAOlB,eAAe,CAACkB,cAAc,CAAC,CAAC;AACzC;;AAEA;;AAEA;AACA,OAAO,SAASC,mBAAmBA,CAACC,QAAgB,EAAmB;EACrE,OAAOpB,eAAe,CAACmB,mBAAmB,CAACC,QAAQ,CAAC;AACtD;;AAEA;AACA,OAAO,SAASC,kBAAkBA,CAAA,EAAoB;EACpD,OAAOrB,eAAe,CAACqB,kBAAkB,CAAC,CAAC;AAC7C;;AAEA;AACA,OAAO,SAASC,iBAAiBA,CAAA,EAAoB;EACnD,OAAOtB,eAAe,CAACsB,iBAAiB,CAAC,CAAC;AAC5C;;AAEA;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASC,wBAAwBA,CACtCC,QAA4C,EAC5C;EACA,OAAOpB,YAAY,CAACqB,WAAW,CAC7BxB,YAAY,CAACC,iBAAiB,EAC9BsB,QACF,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASE,uBAAuBA,CACrCF,QAA4C,EAC5C;EACA,OAAOpB,YAAY,CAACqB,WAAW,CAC7BxB,YAAY,CAACE,sBAAsB,EACnCqB,QACF,CAAC;AACH","ignoreList":[]}
@@ -1,8 +1,11 @@
1
1
  import { type TurboModule } from 'react-native';
2
2
  export interface Spec extends TurboModule {
3
- startScanner(userUUID: string, userId: number | null): Promise<string>;
3
+ startScanner(): Promise<string>;
4
4
  stopScanner(): Promise<string>;
5
5
  isScanning(): Promise<boolean>;
6
+ identify(userId: string): Promise<string>;
7
+ updateProfile(profile: Object): Promise<string>;
8
+ resetIdentity(): Promise<string>;
6
9
  configure(config: Object): Promise<string>;
7
10
  requestNotificationPermissions(): Promise<string>;
8
11
  getDebugLogs(): Promise<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"NativeSpotnySdk.d.ts","sourceRoot":"","sources":["../../../src/NativeSpotnySdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,WAAW,IAAK,SAAQ,WAAW;IAEvC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvE,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/B,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAG/B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAG3C,8BAA8B,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAGlD,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAGlC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAGrC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;;AAED,wBAAmE"}
1
+ {"version":3,"file":"NativeSpotnySdk.d.ts","sourceRoot":"","sources":["../../../src/NativeSpotnySdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,WAAW,IAAK,SAAQ,WAAW;IAEvC,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/B,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAG/B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAGjC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAG3C,8BAA8B,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAGlD,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAGlC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAGrC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;;AAED,wBAAmE"}
@@ -33,16 +33,38 @@ export type SpotnySdkConfig = {
33
33
  */
34
34
  distanceCorrectionFactor?: number;
35
35
  };
36
- /**
37
- * Start beacon scanning.
38
- * @param userUUID A unique string identifying this installation / user session.
39
- * @param userId Optional authenticated user ID.
40
- */
41
- export declare function startScanner(userUUID: string, userId?: number | null): Promise<string>;
36
+ export type SpotnyProfile = {
37
+ /** Phone number (e.g. '+966501234567'). */
38
+ phoneNumber?: string;
39
+ /** Email address. */
40
+ email?: string;
41
+ /** Display name. */
42
+ name?: string;
43
+ };
44
+ /** Start beacon scanning. Call `identify()` before this for attributed tracking. */
45
+ export declare function startScanner(): Promise<string>;
42
46
  /** Stop beacon scanning and clean up all state. */
43
47
  export declare function stopScanner(): Promise<string>;
44
48
  /** Returns `true` if the SDK is currently scanning. */
45
49
  export declare function isScanning(): Promise<boolean>;
50
+ /**
51
+ * Identify the current user. Pass any opaque string your app uses — user ID,
52
+ * hashed email, hashed phone, etc. Stored in Keychain and sent with all
53
+ * subsequent tracking events.
54
+ *
55
+ * Call before `startScanner()` or at any point during an active session.
56
+ */
57
+ export declare function identify(userId: string): Promise<string>;
58
+ /**
59
+ * Update the user's profile on the Spotny backend.
60
+ * Fields are optional — pass only what you have.
61
+ */
62
+ export declare function updateProfile(profile: SpotnyProfile): Promise<string>;
63
+ /**
64
+ * Clear the stored identity (user ID + profile fields) from Keychain.
65
+ * Call on logout. The device ID is NOT affected.
66
+ */
67
+ export declare function resetIdentity(): Promise<string>;
46
68
  /**
47
69
  * Override SDK configuration at runtime.
48
70
  * Must be called **before** `startScanner`.
@@ -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,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC,CAAC;AAOF;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,mDAAmD;AACnD,wBAAgB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAE7C;AAED,uDAAuD;AACvD,wBAAgB,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAE7C;AAID;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAElE;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"}
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,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qBAAqB;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAOF,oFAAoF;AACpF,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;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAExD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAErE;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAE/C;AAID;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAElE;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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spotny-sdk",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Beacon Scanner",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -2,10 +2,15 @@ import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
2
 
3
3
  export interface Spec extends TurboModule {
4
4
  // ── Core scanning ──────────────────────────────────────────────────────────
5
- startScanner(userUUID: string, userId: number | null): Promise<string>;
5
+ startScanner(): Promise<string>;
6
6
  stopScanner(): Promise<string>;
7
7
  isScanning(): Promise<boolean>;
8
8
 
9
+ // ── Identity ───────────────────────────────────────────────────────────────
10
+ identify(userId: string): Promise<string>;
11
+ updateProfile(profile: Object): Promise<string>;
12
+ resetIdentity(): Promise<string>;
13
+
9
14
  // ── Configuration ──────────────────────────────────────────────────────────
10
15
  configure(config: Object): Promise<string>;
11
16
 
package/src/index.tsx CHANGED
@@ -44,21 +44,23 @@ export type SpotnySdkConfig = {
44
44
  distanceCorrectionFactor?: number;
45
45
  };
46
46
 
47
+ export type SpotnyProfile = {
48
+ /** Phone number (e.g. '+966501234567'). */
49
+ phoneNumber?: string;
50
+ /** Email address. */
51
+ email?: string;
52
+ /** Display name. */
53
+ name?: string;
54
+ };
55
+
47
56
  // ── Internal event emitter ───────────────────────────────────────────────────
48
57
  const eventEmitter = new NativeEventEmitter(NativeModules.SpotnySdk);
49
58
 
50
59
  // ── Core scanning ────────────────────────────────────────────────────────────
51
60
 
52
- /**
53
- * Start beacon scanning.
54
- * @param userUUID A unique string identifying this installation / user session.
55
- * @param userId Optional authenticated user ID.
56
- */
57
- export function startScanner(
58
- userUUID: string,
59
- userId?: number | null
60
- ): Promise<string> {
61
- return NativeSpotnySdk.startScanner(userUUID, userId ?? null);
61
+ /** Start beacon scanning. Call `identify()` before this for attributed tracking. */
62
+ export function startScanner(): Promise<string> {
63
+ return NativeSpotnySdk.startScanner();
62
64
  }
63
65
 
64
66
  /** Stop beacon scanning and clean up all state. */
@@ -71,6 +73,35 @@ export function isScanning(): Promise<boolean> {
71
73
  return NativeSpotnySdk.isScanning();
72
74
  }
73
75
 
76
+ // ── Identity ─────────────────────────────────────────────────────────────────
77
+
78
+ /**
79
+ * Identify the current user. Pass any opaque string your app uses — user ID,
80
+ * hashed email, hashed phone, etc. Stored in Keychain and sent with all
81
+ * subsequent tracking events.
82
+ *
83
+ * Call before `startScanner()` or at any point during an active session.
84
+ */
85
+ export function identify(userId: string): Promise<string> {
86
+ return NativeSpotnySdk.identify(userId);
87
+ }
88
+
89
+ /**
90
+ * Update the user's profile on the Spotny backend.
91
+ * Fields are optional — pass only what you have.
92
+ */
93
+ export function updateProfile(profile: SpotnyProfile): Promise<string> {
94
+ return NativeSpotnySdk.updateProfile(profile as Object);
95
+ }
96
+
97
+ /**
98
+ * Clear the stored identity (user ID + profile fields) from Keychain.
99
+ * Call on logout. The device ID is NOT affected.
100
+ */
101
+ export function resetIdentity(): Promise<string> {
102
+ return NativeSpotnySdk.resetIdentity();
103
+ }
104
+
74
105
  // ── Configuration ────────────────────────────────────────────────────────────
75
106
 
76
107
  /**