expo-beacon 0.1.0 → 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.
package/README.md CHANGED
@@ -276,28 +276,46 @@ paired.forEach((b) =>
276
276
 
277
277
  ---
278
278
 
279
- ### `startMonitoring(maxDistance?)`
279
+ ### `startMonitoring(options?)`
280
280
 
281
281
  ```ts
282
- startMonitoring(maxDistance?: number): Promise<void>
282
+ startMonitoring(options?: MonitoringOptions | number): Promise<void>
283
283
  ```
284
284
 
285
285
  Starts background region monitoring for all paired beacons.
286
286
 
287
- | Parameter | Type | Default | Description |
288
- | ------------- | -------- | ------------ | --------------------------------------------------------------------------------------------- |
289
- | `maxDistance` | `number` | `undefined` | Optional distance threshold in metres. `onBeaconEnter` is only emitted when the measured distance is ≤ this value. `onBeaconExit` is always emitted. Omit to disable distance filtering. |
287
+ Accepts either a `MonitoringOptions` object or a plain `number` (backward-compatible shorthand for `maxDistance`).
288
+
289
+ **`MonitoringOptions`**
290
+
291
+ | Property | Type | Default | Description |
292
+ | --------------- | -------------------- | ----------- | --------------------------------------------------------------------------------------------- |
293
+ | `maxDistance` | `number` | `undefined` | Optional distance threshold in metres. `onBeaconEnter` is only emitted when the measured distance is ≤ this value. `onBeaconExit` is always emitted. Omit to disable distance filtering. |
294
+ | `notifications` | `NotificationConfig` | `undefined` | Notification config overrides applied for this session. Persisted and takes effect immediately. |
290
295
 
291
296
  **Android**: Launches `BeaconForegroundService` — a persistent foreground service required by Android 8+ for background BLE. Restarts automatically after device reboot.
292
297
 
293
298
  **iOS**: Activates `CLLocationManager` region monitoring. iOS can wake or relaunch the app when a region boundary is crossed, even if the app is terminated.
294
299
 
295
300
  ```ts
296
- // Monitor with no distance limit
297
- await ExpoBeacon.startMonitoring();
298
-
299
- // Only fire enter events when within 5 metres
301
+ // Backward-compatible shorthand (number = maxDistance)
300
302
  await ExpoBeacon.startMonitoring(5);
303
+
304
+ // Full options object
305
+ await ExpoBeacon.startMonitoring({
306
+ maxDistance: 5,
307
+ notifications: {
308
+ beaconEvents: {
309
+ enterTitle: "Beacon nearby!",
310
+ body: "{identifier} is within range",
311
+ },
312
+ },
313
+ });
314
+
315
+ // Monitor with no distance limit and no enter/exit notifications
316
+ await ExpoBeacon.startMonitoring({
317
+ notifications: { beaconEvents: { enabled: false } },
318
+ });
301
319
  ```
302
320
 
303
321
  > Call `requestPermissionsAsync()` before `startMonitoring()`.
@@ -318,6 +336,48 @@ await ExpoBeacon.stopMonitoring();
318
336
 
319
337
  ---
320
338
 
339
+ ### `setNotificationConfig(config)`
340
+
341
+ ```ts
342
+ setNotificationConfig(config: NotificationConfig): void
343
+ ```
344
+
345
+ Persists notification configuration that is applied to all subsequent monitoring sessions. Values are stored in `SharedPreferences` (Android) / `UserDefaults` (iOS) and survive app restarts.
346
+
347
+ For one-off overrides tied to a single session, pass `notifications` directly in [`startMonitoring(options)`](#startmonitoringoptions) instead — it calls this function automatically.
348
+
349
+ ```ts
350
+ ExpoBeacon.setNotificationConfig({
351
+ // Enter/exit alert notifications
352
+ beaconEvents: {
353
+ enabled: true, // false to suppress all enter/exit notifications
354
+ enterTitle: "Beacon nearby",
355
+ exitTitle: "Beacon out of range",
356
+ body: "{identifier} {event}ed", // {identifier} and {event} are replaced at runtime
357
+ sound: true, // iOS only — default true
358
+ icon: "ic_beacon_notification", // Android only — drawable resource name
359
+ },
360
+
361
+ // Persistent status-bar notification while monitoring is active (Android only)
362
+ foregroundService: {
363
+ title: "My App is watching",
364
+ text: "Monitoring for nearby beacons",
365
+ icon: "ic_service", // Android drawable resource name
366
+ },
367
+
368
+ // Android notification channel settings
369
+ channel: {
370
+ name: "Proximity Alerts",
371
+ description: "Alerts when beacons enter or leave range",
372
+ importance: "default", // "low" | "default" | "high"
373
+ },
374
+ });
375
+ ```
376
+
377
+ > **Android channel importance note**: Android prevents decreasing channel importance once a user has been notified. Increasing importance always works; decreasing it will have no effect until the user clears the app's notification settings or reinstalls the app.
378
+
379
+ ---
380
+
321
381
  ## Events
322
382
 
323
383
  Subscribe to events using `ExpoBeacon.addListener(eventName, handler)`. Always call `.remove()` on the subscription when your component unmounts.
@@ -476,6 +536,71 @@ type BeaconDistanceEvent = {
476
536
 
477
537
  ---
478
538
 
539
+ ### `MonitoringOptions`
540
+
541
+ Passed to `startMonitoring(options)`.
542
+
543
+ ```ts
544
+ type MonitoringOptions = {
545
+ maxDistance?: number; // Distance threshold in metres for enter events
546
+ notifications?: NotificationConfig; // Notification overrides for this session
547
+ };
548
+ ```
549
+
550
+ ### `NotificationConfig`
551
+
552
+ Top-level config object accepted by `setNotificationConfig()` and `startMonitoring({ notifications })`.
553
+
554
+ ```ts
555
+ type NotificationConfig = {
556
+ beaconEvents?: BeaconNotificationConfig;
557
+ foregroundService?: ForegroundServiceConfig; // Android only
558
+ channel?: NotificationChannelConfig; // Android only
559
+ };
560
+ ```
561
+
562
+ ### `BeaconNotificationConfig`
563
+
564
+ Controls the enter/exit alert notifications.
565
+
566
+ ```ts
567
+ type BeaconNotificationConfig = {
568
+ enabled?: boolean; // false to disable all enter/exit notifications. Default: true
569
+ enterTitle?: string; // Default: "Beacon Entered"
570
+ exitTitle?: string; // Default: "Beacon Exited"
571
+ body?: string; // Template — {identifier} and {event} are replaced at runtime
572
+ // Default: "{identifier} region {event}ed"
573
+ sound?: boolean; // iOS only — play notification sound. Default: true
574
+ icon?: string; // Android only — drawable resource name (e.g. "ic_notification")
575
+ };
576
+ ```
577
+
578
+ ### `ForegroundServiceConfig`
579
+
580
+ Controls the persistent Android status-bar notification while monitoring is active.
581
+
582
+ ```ts
583
+ type ForegroundServiceConfig = {
584
+ title?: string; // Default: "Beacon Monitoring Active"
585
+ text?: string; // Default: "Monitoring for iBeacons in the background"
586
+ icon?: string; // Android drawable resource name
587
+ };
588
+ ```
589
+
590
+ ### `NotificationChannelConfig`
591
+
592
+ Controls the Android notification channel shown in system settings.
593
+
594
+ ```ts
595
+ type NotificationChannelConfig = {
596
+ name?: string; // Default: "Beacon Monitoring"
597
+ description?: string; // Default: "Used for background iBeacon region monitoring"
598
+ importance?: "low" | "default" | "high"; // Default: "low"
599
+ };
600
+ ```
601
+
602
+ ---
603
+
479
604
  ## Background Behaviour
480
605
 
481
606
  ### Android
@@ -494,14 +619,30 @@ Default scan timing: 1.1 s scan window every 5 s.
494
619
 
495
620
  ## Notifications
496
621
 
497
- A local notification is posted for every `onBeaconEnter` and `onBeaconExit` event.
622
+ A local notification is posted for every `onBeaconEnter` and `onBeaconExit` event. All notification settings can be customised via [`setNotificationConfig()`](#setnotificationconfigconfig) or inline in [`startMonitoring(options)`](#startmonitoringoptions).
623
+
624
+ ### Defaults
625
+
626
+ | Property | Default value |
627
+ | ------------------------------ | ------------------------------------------------ |
628
+ | Enter title | `"Beacon Entered"` |
629
+ | Exit title | `"Beacon Exited"` |
630
+ | Body | `"{identifier} region {event}ed"` |
631
+ | Sound (iOS) | `true` |
632
+ | Icon (Android) | System `ic_dialog_info` |
633
+ | Foreground service title | `"Beacon Monitoring Active"` |
634
+ | Foreground service text | `"Monitoring for iBeacons in the background"` |
635
+ | Channel name (Android) | `"Beacon Monitoring"` |
636
+ | Channel importance (Android) | `"low"` |
637
+
638
+ ### Channel IDs (Android)
498
639
 
499
640
  | Channel / type | Importance |
500
641
  | ------------------------------- | ------------------- |
501
- | Foreground service (Android) | `IMPORTANCE_LOW` |
502
- | Enter / exit alerts | `IMPORTANCE_DEFAULT`|
642
+ | Foreground service (Android) | configurable (default `low`) |
643
+ | Enter / exit alerts | configurable (default `default`) |
503
644
 
504
- Both channels share the id `expo_beacon_channel`.
645
+ Both notifications share the channel id `expo_beacon_channel`. The channel is recreated on each `onStartCommand` so config changes take effect on the next monitoring start.
505
646
 
506
647
  ---
507
648
 
@@ -70,7 +70,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
70
70
  )
71
71
  }
72
72
  // Use continuous scanning (not JobScheduler) for foreground service
73
- manager.setEnableScheduledScanJobs(false)
73
+ // Guard: throws if called after ranging/monitoring has already started
74
+ try { manager.setEnableScheduledScanJobs(false) } catch (_: IllegalStateException) {}
74
75
  manager.setBackgroundBetweenScanPeriod(5000L) // 5s between scans
75
76
  manager.setBackgroundScanPeriod(1100L) // 1.1s scan window
76
77
  manager.setForegroundScanPeriod(1000L) // 1s scan window for distance logging
@@ -231,11 +232,33 @@ class BeaconForegroundService : Service(), BeaconConsumer {
231
232
  }
232
233
 
233
234
  private fun showEnterExitNotification(region: Region, eventType: String) {
234
- val title = if (eventType == "enter") "Beacon Entered" else "Beacon Exited"
235
- val message = "${region.uniqueId} region ${eventType}ed"
235
+ val config = readNotificationConfig()
236
+ val eventsConfig = config.optJSONObject("beaconEvents")
237
+
238
+ // Respect the enabled flag (defaults to true)
239
+ if (eventsConfig != null && !eventsConfig.optBoolean("enabled", true)) return
240
+
241
+ val defaultTitle = if (eventType == "enter") "Beacon Entered" else "Beacon Exited"
242
+ val title = if (eventType == "enter") {
243
+ eventsConfig?.optString("enterTitle")?.takeIf { it.isNotEmpty() } ?: defaultTitle
244
+ } else {
245
+ eventsConfig?.optString("exitTitle")?.takeIf { it.isNotEmpty() } ?: defaultTitle
246
+ }
247
+
248
+ val bodyTemplate = eventsConfig?.optString("body")?.takeIf { it.isNotEmpty() }
249
+ ?: "{identifier} region {event}ed"
250
+ val message = bodyTemplate
251
+ .replace("{identifier}", region.uniqueId)
252
+ .replace("{event}", eventType)
253
+
254
+ val iconName = eventsConfig?.optString("icon")?.takeIf { it.isNotEmpty() }
255
+ val iconResId = iconName?.let { name ->
256
+ try { resources.getIdentifier(name, "drawable", packageName).takeIf { it != 0 } }
257
+ catch (_: Exception) { null }
258
+ } ?: android.R.drawable.ic_dialog_info
236
259
 
237
260
  val notification = NotificationCompat.Builder(this, CHANNEL_ID)
238
- .setSmallIcon(android.R.drawable.ic_dialog_info)
261
+ .setSmallIcon(iconResId)
239
262
  .setContentTitle(title)
240
263
  .setContentText(message)
241
264
  .setPriority(NotificationCompat.PRIORITY_DEFAULT)
@@ -251,27 +274,58 @@ class BeaconForegroundService : Service(), BeaconConsumer {
251
274
 
252
275
  private fun createNotificationChannel() {
253
276
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
254
- val channel = NotificationChannel(
255
- CHANNEL_ID,
256
- "Beacon Monitoring",
257
- NotificationManager.IMPORTANCE_LOW
258
- ).apply {
259
- description = "Used for background iBeacon region monitoring"
277
+ val config = readNotificationConfig()
278
+ val channelConfig = config.optJSONObject("channel")
279
+
280
+ val channelName = channelConfig?.optString("name")?.takeIf { it.isNotEmpty() }
281
+ ?: "Beacon Monitoring"
282
+ val channelDesc = channelConfig?.optString("description")?.takeIf { it.isNotEmpty() }
283
+ ?: "Used for background iBeacon region monitoring"
284
+ val importance = when (channelConfig?.optString("importance")) {
285
+ "high" -> NotificationManager.IMPORTANCE_HIGH
286
+ "default" -> NotificationManager.IMPORTANCE_DEFAULT
287
+ else -> NotificationManager.IMPORTANCE_LOW
260
288
  }
261
- getSystemService(NotificationManager::class.java)?.createNotificationChannel(channel)
289
+
290
+ val notifMgr = getSystemService(NotificationManager::class.java)
291
+ // Delete and recreate so config changes take effect
292
+ notifMgr?.deleteNotificationChannel(CHANNEL_ID)
293
+ val channel = NotificationChannel(CHANNEL_ID, channelName, importance).apply {
294
+ description = channelDesc
295
+ }
296
+ notifMgr?.createNotificationChannel(channel)
262
297
  }
263
298
  }
264
299
 
265
300
  private fun buildForegroundNotification(): Notification {
301
+ val config = readNotificationConfig()
302
+ val fgConfig = config.optJSONObject("foregroundService")
303
+
304
+ val title = fgConfig?.optString("title")?.takeIf { it.isNotEmpty() }
305
+ ?: "Beacon Monitoring Active"
306
+ val text = fgConfig?.optString("text")?.takeIf { it.isNotEmpty() }
307
+ ?: "Monitoring for iBeacons in the background"
308
+ val iconName = fgConfig?.optString("icon")?.takeIf { it.isNotEmpty() }
309
+ val iconResId = iconName?.let { name ->
310
+ try { resources.getIdentifier(name, "drawable", packageName).takeIf { it != 0 } }
311
+ catch (_: Exception) { null }
312
+ } ?: android.R.drawable.ic_dialog_info
313
+
266
314
  return NotificationCompat.Builder(this, CHANNEL_ID)
267
- .setSmallIcon(android.R.drawable.ic_dialog_info)
268
- .setContentTitle("Beacon Monitoring Active")
269
- .setContentText("Monitoring for iBeacons in the background")
315
+ .setSmallIcon(iconResId)
316
+ .setContentTitle(title)
317
+ .setContentText(text)
270
318
  .setPriority(NotificationCompat.PRIORITY_LOW)
271
319
  .setOngoing(true)
272
320
  .build()
273
321
  }
274
322
 
323
+ private fun readNotificationConfig(): org.json.JSONObject {
324
+ val json = getSharedPreferences("expo.beacon.notification_config", Context.MODE_PRIVATE)
325
+ .getString("config", null) ?: return org.json.JSONObject()
326
+ return try { org.json.JSONObject(json) } catch (_: Exception) { org.json.JSONObject() }
327
+ }
328
+
275
329
  override fun onDestroy() {
276
330
  beaconManager.removeMonitorNotifier(monitorNotifier)
277
331
  beaconManager.removeRangeNotifier(rangeNotifier)
@@ -21,15 +21,20 @@ import org.json.JSONObject
21
21
 
22
22
  private const val PREFS_NAME = "expo.beacon.paired"
23
23
  private const val PREFS_KEY = "paired_beacons"
24
+ private const val NOTIFICATION_CONFIG_PREFS = "expo.beacon.notification_config"
24
25
 
25
26
  class ExpoBeaconModule : Module(), BeaconConsumer {
26
27
 
27
28
  private val beaconManager: BeaconManager by lazy {
28
29
  BeaconManager.getInstanceForApplication(appContext.reactContext!!).also { manager ->
30
+ // Must be configured before any bind/ranging starts
31
+ manager.setEnableScheduledScanJobs(false)
29
32
  // Register iBeacon layout parser
30
- manager.beaconParsers.add(
31
- BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25")
32
- )
33
+ if (manager.beaconParsers.none { it.layout?.contains("0215") == true }) {
34
+ manager.beaconParsers.add(
35
+ BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25")
36
+ )
37
+ }
33
38
  }
34
39
  }
35
40
 
@@ -149,8 +154,23 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
149
154
  }
150
155
  }
151
156
 
152
- AsyncFunction("startMonitoring") { maxDistance: Double?, promise: Promise ->
157
+ AsyncFunction("startMonitoring") { options: Any?, promise: Promise ->
153
158
  val ctx = appContext.reactContext!!
159
+ var maxDistance: Double? = null
160
+ when (options) {
161
+ is Double -> maxDistance = options
162
+ is Map<*, *> -> {
163
+ @Suppress("UNCHECKED_CAST")
164
+ val map = options as Map<String, Any?>
165
+ maxDistance = (map["maxDistance"] as? Number)?.toDouble()
166
+ val notifications = map["notifications"]
167
+ if (notifications is Map<*, *>) {
168
+ @Suppress("UNCHECKED_CAST")
169
+ ctx.getSharedPreferences(NOTIFICATION_CONFIG_PREFS, Context.MODE_PRIVATE)
170
+ .edit().putString("config", mapToJson(notifications as Map<String, Any?>).toString()).apply()
171
+ }
172
+ }
173
+ }
154
174
  ctx.getSharedPreferences("expo.beacon.monitoring_options", Context.MODE_PRIVATE)
155
175
  .edit().apply {
156
176
  if (maxDistance != null) putFloat("max_distance", maxDistance.toFloat())
@@ -161,6 +181,13 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
161
181
  promise.resolve(null)
162
182
  }
163
183
 
184
+ Function("setNotificationConfig") { config: Map<String, Any?> ->
185
+ val ctx = appContext.reactContext!!
186
+ ctx.getSharedPreferences(NOTIFICATION_CONFIG_PREFS, Context.MODE_PRIVATE)
187
+ .edit().putString("config", mapToJson(config).toString()).apply()
188
+ null
189
+ }
190
+
164
191
  AsyncFunction("stopMonitoring") { promise: Promise ->
165
192
  BeaconForegroundService.stop(appContext.reactContext!!)
166
193
  unregisterEventReceiver()
@@ -284,6 +311,23 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
284
311
  scanPromise = null
285
312
  }
286
313
 
314
+ // --- Notification config helpers ---
315
+
316
+ private fun mapToJson(map: Map<String, Any?>): JSONObject {
317
+ val json = JSONObject()
318
+ map.forEach { (key, value) ->
319
+ when (value) {
320
+ null -> json.put(key, JSONObject.NULL)
321
+ is Map<*, *> -> {
322
+ @Suppress("UNCHECKED_CAST")
323
+ json.put(key, mapToJson(value as Map<String, Any?>))
324
+ }
325
+ else -> json.put(key, value)
326
+ }
327
+ }
328
+ return json
329
+ }
330
+
287
331
  // --- Shared Preferences helpers ---
288
332
 
289
333
  private fun loadPairedBeaconsJson(): JSONArray {
@@ -41,6 +41,64 @@ export type BeaconDistanceEvent = {
41
41
  minor: number;
42
42
  distance: number;
43
43
  };
44
+ /** Configuration for beacon enter/exit event notifications. */
45
+ export type BeaconNotificationConfig = {
46
+ /** Whether to show enter/exit notifications. Default: true. */
47
+ enabled?: boolean;
48
+ /** Notification title on beacon enter. Default: "Beacon Entered". */
49
+ enterTitle?: string;
50
+ /** Notification title on beacon exit. Default: "Beacon Exited". */
51
+ exitTitle?: string;
52
+ /**
53
+ * Notification body template. Supports {identifier} and {event} placeholders.
54
+ * Default: "{identifier} region {event}ed".
55
+ */
56
+ body?: string;
57
+ /** Play a sound with the notification (iOS only). Default: true. */
58
+ sound?: boolean;
59
+ /** Android drawable resource name for the notification icon (e.g. "ic_notification"). */
60
+ icon?: string;
61
+ };
62
+ /** Configuration for the Android foreground service notification (persistent status bar entry). */
63
+ export type ForegroundServiceConfig = {
64
+ /** Title of the persistent notification. Default: "Beacon Monitoring Active". */
65
+ title?: string;
66
+ /** Body text of the persistent notification. Default: "Monitoring for iBeacons in the background". */
67
+ text?: string;
68
+ /** Android drawable resource name for the notification icon. */
69
+ icon?: string;
70
+ };
71
+ /** Configuration for the Android notification channel. */
72
+ export type NotificationChannelConfig = {
73
+ /** Channel display name shown in system settings. Default: "Beacon Monitoring". */
74
+ name?: string;
75
+ /** Channel description shown in system settings. Default: "Used for background iBeacon region monitoring". */
76
+ description?: string;
77
+ /**
78
+ * Channel importance level. Default: 'low'.
79
+ * Note: Android may ignore decreases in importance after first channel creation until the app is reinstalled.
80
+ */
81
+ importance?: "low" | "default" | "high";
82
+ };
83
+ /** Combined notification configuration for all notification types. */
84
+ export type NotificationConfig = {
85
+ /** Settings for beacon enter/exit event notifications. */
86
+ beaconEvents?: BeaconNotificationConfig;
87
+ /** Settings for the persistent foreground service notification (Android only). */
88
+ foregroundService?: ForegroundServiceConfig;
89
+ /** Settings for the Android notification channel (Android only). */
90
+ channel?: NotificationChannelConfig;
91
+ };
92
+ /** Options accepted by startMonitoring(). */
93
+ export type MonitoringOptions = {
94
+ /**
95
+ * Maximum distance in metres for distance-based enter events.
96
+ * Exit events are always emitted when the region is lost.
97
+ */
98
+ maxDistance?: number;
99
+ /** Notification configuration overrides to apply for this monitoring session. */
100
+ notifications?: NotificationConfig;
101
+ };
44
102
  /** Module event map. */
45
103
  export type ExpoBeaconModuleEvents = {
46
104
  onBeaconEnter: (params: BeaconRegionEvent) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeacon.types.d.ts","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,+DAA+D;AAC/D,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,4DAA4D;AAC5D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,qEAAqE;AACrE,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAwB;AACxB,MAAM,MAAM,sBAAsB,GAAG;IACnC,aAAa,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACnD,YAAY,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,eAAe,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACtD,gBAAgB,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACxD,wEAAwE;IACxE,aAAa,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACnD,CAAC"}
1
+ {"version":3,"file":"ExpoBeacon.types.d.ts","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,+DAA+D;AAC/D,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,4DAA4D;AAC5D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,qEAAqE;AACrE,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,+DAA+D;AAC/D,MAAM,MAAM,wBAAwB,GAAG;IACrC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,yFAAyF;IACzF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,mGAAmG;AACnG,MAAM,MAAM,uBAAuB,GAAG;IACpC,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sGAAsG;IACtG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,0DAA0D;AAC1D,MAAM,MAAM,yBAAyB,GAAG;IACtC,mFAAmF;IACnF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8GAA8G;IAC9G,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAAC;CACzC,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,0DAA0D;IAC1D,YAAY,CAAC,EAAE,wBAAwB,CAAC;IACxC,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,uBAAuB,CAAC;IAC5C,oEAAoE;IACpE,OAAO,CAAC,EAAE,yBAAyB,CAAC;CACrC,CAAC;AAEF,6CAA6C;AAC7C,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC,CAAC;AAEF,wBAAwB;AACxB,MAAM,MAAM,sBAAsB,GAAG;IACnC,aAAa,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACnD,YAAY,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,eAAe,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACtD,gBAAgB,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACxD,wEAAwE;IACxE,aAAa,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACnD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeacon.types.js","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"","sourcesContent":["/** Raw beacon discovered during a scan. */\r\nexport type BeaconScanResult = {\r\n uuid: string; // iBeacon proximity UUID (uppercase, formatted)\r\n major: number; // iBeacon major value (0–65535)\r\n minor: number; // iBeacon minor value (0–65535)\r\n rssi: number; // Signal strength in dBm (negative number)\r\n distance: number; // Estimated distance in meters\r\n txPower: number; // Calibrated TX power\r\n};\r\n\r\n/** A beacon that has been paired/registered for monitoring. */\r\nexport type PairedBeacon = {\r\n identifier: string; // User-defined label (e.g. \"lobby-door\")\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n};\r\n\r\n/** Payload for enter/exit region events. */\r\nexport type BeaconRegionEvent = {\r\n identifier: string; // Matches PairedBeacon.identifier\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n event: \"enter\" | \"exit\";\r\n /** Measured distance in metres at the time of the event (–1 if unavailable). */\r\n distance: number;\r\n};\r\n\r\n/** Payload for ranging events (beacon proximity update). */\r\nexport type BeaconRangingEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n rssi: number;\r\n distance: number;\r\n};\r\n\r\n/** Payload for periodic distance update events during monitoring. */\r\nexport type BeaconDistanceEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n distance: number;\r\n};\r\n\r\n/** Module event map. */\r\nexport type ExpoBeaconModuleEvents = {\r\n onBeaconEnter: (params: BeaconRegionEvent) => void;\r\n onBeaconExit: (params: BeaconRegionEvent) => void;\r\n onBeaconRanging: (params: BeaconRangingEvent) => void;\r\n onBeaconDistance: (params: BeaconDistanceEvent) => void;\r\n /** Fired continuously during a live scan as each beacon is detected. */\r\n onBeaconFound: (params: BeaconScanResult) => void;\r\n};\r\n"]}
1
+ {"version":3,"file":"ExpoBeacon.types.js","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"","sourcesContent":["/** Raw beacon discovered during a scan. */\r\nexport type BeaconScanResult = {\r\n uuid: string; // iBeacon proximity UUID (uppercase, formatted)\r\n major: number; // iBeacon major value (0–65535)\r\n minor: number; // iBeacon minor value (0–65535)\r\n rssi: number; // Signal strength in dBm (negative number)\r\n distance: number; // Estimated distance in meters\r\n txPower: number; // Calibrated TX power\r\n};\r\n\r\n/** A beacon that has been paired/registered for monitoring. */\r\nexport type PairedBeacon = {\r\n identifier: string; // User-defined label (e.g. \"lobby-door\")\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n};\r\n\r\n/** Payload for enter/exit region events. */\r\nexport type BeaconRegionEvent = {\r\n identifier: string; // Matches PairedBeacon.identifier\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n event: \"enter\" | \"exit\";\r\n /** Measured distance in metres at the time of the event (–1 if unavailable). */\r\n distance: number;\r\n};\r\n\r\n/** Payload for ranging events (beacon proximity update). */\r\nexport type BeaconRangingEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n rssi: number;\r\n distance: number;\r\n};\r\n\r\n/** Payload for periodic distance update events during monitoring. */\r\nexport type BeaconDistanceEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n distance: number;\r\n};\r\n\r\n/** Configuration for beacon enter/exit event notifications. */\r\nexport type BeaconNotificationConfig = {\r\n /** Whether to show enter/exit notifications. Default: true. */\r\n enabled?: boolean;\r\n /** Notification title on beacon enter. Default: \"Beacon Entered\". */\r\n enterTitle?: string;\r\n /** Notification title on beacon exit. Default: \"Beacon Exited\". */\r\n exitTitle?: string;\r\n /**\r\n * Notification body template. Supports {identifier} and {event} placeholders.\r\n * Default: \"{identifier} region {event}ed\".\r\n */\r\n body?: string;\r\n /** Play a sound with the notification (iOS only). Default: true. */\r\n sound?: boolean;\r\n /** Android drawable resource name for the notification icon (e.g. \"ic_notification\"). */\r\n icon?: string;\r\n};\r\n\r\n/** Configuration for the Android foreground service notification (persistent status bar entry). */\r\nexport type ForegroundServiceConfig = {\r\n /** Title of the persistent notification. Default: \"Beacon Monitoring Active\". */\r\n title?: string;\r\n /** Body text of the persistent notification. Default: \"Monitoring for iBeacons in the background\". */\r\n text?: string;\r\n /** Android drawable resource name for the notification icon. */\r\n icon?: string;\r\n};\r\n\r\n/** Configuration for the Android notification channel. */\r\nexport type NotificationChannelConfig = {\r\n /** Channel display name shown in system settings. Default: \"Beacon Monitoring\". */\r\n name?: string;\r\n /** Channel description shown in system settings. Default: \"Used for background iBeacon region monitoring\". */\r\n description?: string;\r\n /**\r\n * Channel importance level. Default: 'low'.\r\n * Note: Android may ignore decreases in importance after first channel creation until the app is reinstalled.\r\n */\r\n importance?: \"low\" | \"default\" | \"high\";\r\n};\r\n\r\n/** Combined notification configuration for all notification types. */\r\nexport type NotificationConfig = {\r\n /** Settings for beacon enter/exit event notifications. */\r\n beaconEvents?: BeaconNotificationConfig;\r\n /** Settings for the persistent foreground service notification (Android only). */\r\n foregroundService?: ForegroundServiceConfig;\r\n /** Settings for the Android notification channel (Android only). */\r\n channel?: NotificationChannelConfig;\r\n};\r\n\r\n/** Options accepted by startMonitoring(). */\r\nexport type MonitoringOptions = {\r\n /**\r\n * Maximum distance in metres for distance-based enter events.\r\n * Exit events are always emitted when the region is lost.\r\n */\r\n maxDistance?: number;\r\n /** Notification configuration overrides to apply for this monitoring session. */\r\n notifications?: NotificationConfig;\r\n};\r\n\r\n/** Module event map. */\r\nexport type ExpoBeaconModuleEvents = {\r\n onBeaconEnter: (params: BeaconRegionEvent) => void;\r\n onBeaconExit: (params: BeaconRegionEvent) => void;\r\n onBeaconRanging: (params: BeaconRangingEvent) => void;\r\n onBeaconDistance: (params: BeaconDistanceEvent) => void;\r\n /** Fired continuously during a live scan as each beacon is detected. */\r\n onBeaconFound: (params: BeaconScanResult) => void;\r\n};\r\n"]}
@@ -1,46 +1,2 @@
1
- import { NativeModule } from "expo";
2
- import { ExpoBeaconModuleEvents, BeaconScanResult, PairedBeacon } from "./ExpoBeacon.types";
3
- declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
4
- /**
5
- * Start a one-shot BLE scan. Resolves with discovered beacons after scanDuration ms.
6
- * @param scanDuration Duration in ms (default 5000)
7
- */
8
- scanForBeaconsAsync(scanDuration?: number): Promise<BeaconScanResult[]>;
9
- /**
10
- * Register a beacon for persistent region monitoring.
11
- */
12
- pairBeacon(identifier: string, uuid: string, major: number, minor: number): void;
13
- /**
14
- * Remove a previously paired beacon.
15
- */
16
- unpairBeacon(identifier: string): void;
17
- /**
18
- * Return all currently paired beacons.
19
- */
20
- getPairedBeacons(): PairedBeacon[];
21
- /**
22
- * Start background region monitoring for all paired beacons.
23
- * On Android starts a foreground service.
24
- * On iOS starts CLLocationManager region monitoring.
25
- * @param maxDistance Optional distance threshold in metres. Enter events are only
26
- * emitted when the beacon is measured to be within this distance.
27
- * Exit events are always emitted when the beacon region is lost.
28
- */
29
- startMonitoring(maxDistance?: number): Promise<void>;
30
- /**
31
- * Stop background region monitoring.
32
- */
33
- stopMonitoring(): Promise<void>;
34
- /**
35
- * Start a continuous BLE scan. Fires `onBeaconFound` events as beacons are detected.
36
- * Call stopContinuousScan() to end the scan.
37
- */
38
- startContinuousScan(): void;
39
- /** Stop the continuous scan started by startContinuousScan(). */
40
- stopContinuousScan(): void;
41
- /** Request Bluetooth + Location permissions. Returns true if granted. */
42
- requestPermissionsAsync(): Promise<boolean>;
43
- }
44
- declare const _default: ExpoBeaconModule;
45
- export default _default;
1
+ export default module;
46
2
  //# sourceMappingURL=ExpoBeaconModule.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,YAAY,EACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,OAAO,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IACzE;;;OAGG;IACH,mBAAmB,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAEvE;;OAEG;IACH,UAAU,CACR,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACZ,IAAI;IAEP;;OAEG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEtC;;OAEG;IACH,gBAAgB,IAAI,YAAY,EAAE;IAElC;;;;;;;OAOG;IACH,eAAe,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEpD;;OAEG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAE/B;;;OAGG;IACH,mBAAmB,IAAI,IAAI;IAE3B,iEAAiE;IACjE,kBAAkB,IAAI,IAAI;IAE1B,yEAAyE;IACzE,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC;CAC5C;;AAED,wBAAmE"}
1
+ {"version":3,"file":"ExpoBeaconModule.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAiFA,eAAe,MAAM,CAAC"}
@@ -1,3 +1,11 @@
1
1
  import { requireNativeModule } from "expo";
2
- export default requireNativeModule("ExpoBeacon");
2
+ try {
3
+ // eslint-disable-next-line import/no-mutable-exports
4
+ var module = requireNativeModule("ExpoBeacon");
5
+ }
6
+ catch {
7
+ throw new Error("expo-beacon: native module not found. Make sure you are using a development build " +
8
+ "(not Expo Go) and have run `npx expo prebuild` followed by a native rebuild.");
9
+ }
10
+ export default module;
3
11
  //# sourceMappingURL=ExpoBeaconModule.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AA+DzD,eAAe,mBAAmB,CAAmB,YAAY,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\r\n\r\nimport {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n PairedBeacon,\r\n} from \"./ExpoBeacon.types\";\r\n\r\ndeclare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {\r\n /**\r\n * Start a one-shot BLE scan. Resolves with discovered beacons after scanDuration ms.\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForBeaconsAsync(scanDuration?: number): Promise<BeaconScanResult[]>;\r\n\r\n /**\r\n * Register a beacon for persistent region monitoring.\r\n */\r\n pairBeacon(\r\n identifier: string,\r\n uuid: string,\r\n major: number,\r\n minor: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired beacon.\r\n */\r\n unpairBeacon(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired beacons.\r\n */\r\n getPairedBeacons(): PairedBeacon[];\r\n\r\n /**\r\n * Start background region monitoring for all paired beacons.\r\n * On Android starts a foreground service.\r\n * On iOS starts CLLocationManager region monitoring.\r\n * @param maxDistance Optional distance threshold in metres. Enter events are only\r\n * emitted when the beacon is measured to be within this distance.\r\n * Exit events are always emitted when the beacon region is lost.\r\n */\r\n startMonitoring(maxDistance?: number): Promise<void>;\r\n\r\n /**\r\n * Stop background region monitoring.\r\n */\r\n stopMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Start a continuous BLE scan. Fires `onBeaconFound` events as beacons are detected.\r\n * Call stopContinuousScan() to end the scan.\r\n */\r\n startContinuousScan(): void;\r\n\r\n /** Stop the continuous scan started by startContinuousScan(). */\r\n stopContinuousScan(): void;\r\n\r\n /** Request Bluetooth + Location permissions. Returns true if granted. */\r\n requestPermissionsAsync(): Promise<boolean>;\r\n}\r\n\r\nexport default requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n"]}
1
+ {"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAuEzD,IAAI,CAAC;IACH,qDAAqD;IACrD,IAAI,MAAM,GAAG,mBAAmB,CAAmB,YAAY,CAAC,CAAC;AACnE,CAAC;AAAC,MAAM,CAAC;IACP,MAAM,IAAI,KAAK,CACb,oFAAoF;QAClF,8EAA8E,CACjF,CAAC;AACJ,CAAC;AAED,eAAe,MAAM,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\r\n\r\nimport {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n PairedBeacon,\r\n NotificationConfig,\r\n MonitoringOptions,\r\n} from \"./ExpoBeacon.types\";\r\n\r\ndeclare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {\r\n /**\r\n * Start a one-shot BLE scan. Resolves with discovered beacons after scanDuration ms.\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForBeaconsAsync(scanDuration?: number): Promise<BeaconScanResult[]>;\r\n\r\n /**\r\n * Register a beacon for persistent region monitoring.\r\n */\r\n pairBeacon(\r\n identifier: string,\r\n uuid: string,\r\n major: number,\r\n minor: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired beacon.\r\n */\r\n unpairBeacon(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired beacons.\r\n */\r\n getPairedBeacons(): PairedBeacon[];\r\n\r\n /**\r\n * Set persistent notification configuration. Settings are saved and applied to all\r\n * subsequent monitoring sessions until explicitly changed.\r\n */\r\n setNotificationConfig(config: NotificationConfig): void;\r\n\r\n /**\r\n * Start background region monitoring for all paired beacons.\r\n * On Android starts a foreground service.\r\n * On iOS starts CLLocationManager region monitoring.\r\n *\r\n * Accepts a plain number (backward-compatible maxDistance shorthand) or a\r\n * MonitoringOptions object with maxDistance and/or notification overrides.\r\n */\r\n startMonitoring(options?: MonitoringOptions | number): Promise<void>;\r\n\r\n /**\r\n * Stop background region monitoring.\r\n */\r\n stopMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Start a continuous BLE scan. Fires `onBeaconFound` events as beacons are detected.\r\n * Call stopContinuousScan() to end the scan.\r\n */\r\n startContinuousScan(): void;\r\n\r\n /** Stop the continuous scan started by startContinuousScan(). */\r\n stopContinuousScan(): void;\r\n\r\n /** Request Bluetooth + Location permissions. Returns true if granted. */\r\n requestPermissionsAsync(): Promise<boolean>;\r\n}\r\n\r\ntry {\r\n // eslint-disable-next-line import/no-mutable-exports\r\n var module = requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n} catch {\r\n throw new Error(\r\n \"expo-beacon: native module not found. Make sure you are using a development build \" +\r\n \"(not Expo Go) and have run `npx expo prebuild` followed by a native rebuild.\",\r\n );\r\n}\r\n\r\nexport default module;\r\n"]}
package/build/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default } from "./ExpoBeaconModule";
2
- export type { BeaconScanResult, PairedBeacon, BeaconRegionEvent, BeaconRangingEvent, ExpoBeaconModuleEvents, } from "./ExpoBeacon.types";
2
+ export type { BeaconScanResult, PairedBeacon, BeaconRegionEvent, BeaconRangingEvent, BeaconDistanceEvent, ExpoBeaconModuleEvents, NotificationConfig, MonitoringOptions, BeaconNotificationConfig, ForegroundServiceConfig, NotificationChannelConfig, } from "./ExpoBeacon.types";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,YAAY,EACV,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,YAAY,EACV,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,wBAAwB,EACxB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,oBAAoB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC","sourcesContent":["// Native module (default export)\r\nexport { default } from \"./ExpoBeaconModule\";\r\n\r\n// All public types\r\nexport type {\r\n BeaconScanResult,\r\n PairedBeacon,\r\n BeaconRegionEvent,\r\n BeaconRangingEvent,\r\n ExpoBeaconModuleEvents,\r\n} from \"./ExpoBeacon.types\";\r\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC","sourcesContent":["// Native module (default export)\r\nexport { default } from \"./ExpoBeaconModule\";\r\n\r\n// All public types\r\nexport type {\r\n BeaconScanResult,\r\n PairedBeacon,\r\n BeaconRegionEvent,\r\n BeaconRangingEvent,\r\n BeaconDistanceEvent,\r\n ExpoBeaconModuleEvents,\r\n NotificationConfig,\r\n MonitoringOptions,\r\n BeaconNotificationConfig,\r\n ForegroundServiceConfig,\r\n NotificationChannelConfig,\r\n} from \"./ExpoBeacon.types\";\r\n"]}
@@ -5,8 +5,9 @@ import UserNotifications
5
5
  private let PAIRED_BEACONS_KEY = "expo.beacon.paired"
6
6
  private let IS_MONITORING_KEY = "expo.beacon.is_monitoring"
7
7
  private let MAX_DISTANCE_KEY = "expo.beacon.max_distance"
8
+ private let NOTIFICATION_CONFIG_KEY = "expo.beacon.notification_config"
8
9
 
9
- public class ExpoBeaconModule: Module, CLLocationManagerDelegate {
10
+ public class ExpoBeaconModule: NSObject, Module, CLLocationManagerDelegate {
10
11
 
11
12
  private lazy var locationManager: CLLocationManager = {
12
13
  let manager = CLLocationManager()
@@ -97,9 +98,29 @@ public class ExpoBeaconModule: Module, CLLocationManagerDelegate {
97
98
  return self.loadPairedBeaconsRaw()
98
99
  }
99
100
 
101
+ // MARK: - Notification Config
102
+
103
+ Function("setNotificationConfig") { (config: [String: Any]) in
104
+ if let data = try? JSONSerialization.data(withJSONObject: config),
105
+ let json = String(data: data, encoding: .utf8) {
106
+ UserDefaults.standard.set(json, forKey: NOTIFICATION_CONFIG_KEY)
107
+ }
108
+ }
109
+
100
110
  // MARK: - Monitoring
101
111
 
102
- AsyncFunction("startMonitoring") { (maxDistance: Double?, promise: Promise) in
112
+ AsyncFunction("startMonitoring") { (options: Any?, promise: Promise) in
113
+ var maxDistance: Double? = nil
114
+ if let dist = options as? Double {
115
+ maxDistance = dist
116
+ } else if let map = options as? [String: Any] {
117
+ maxDistance = map["maxDistance"] as? Double
118
+ if let notifications = map["notifications"] as? [String: Any],
119
+ let data = try? JSONSerialization.data(withJSONObject: notifications),
120
+ let json = String(data: data, encoding: .utf8) {
121
+ UserDefaults.standard.set(json, forKey: NOTIFICATION_CONFIG_KEY)
122
+ }
123
+ }
103
124
  if let dist = maxDistance {
104
125
  UserDefaults.standard.set(dist, forKey: MAX_DISTANCE_KEY)
105
126
  } else {
@@ -274,10 +295,32 @@ public class ExpoBeaconModule: Module, CLLocationManagerDelegate {
274
295
  }
275
296
 
276
297
  private func postBeaconNotification(identifier: String, eventType: String) {
298
+ let cfg = loadNotificationConfig()
299
+ let eventsCfg = cfg["beaconEvents"] as? [String: Any]
300
+
301
+ // Respect the enabled flag (defaults to true)
302
+ if let enabled = eventsCfg?["enabled"] as? Bool, !enabled { return }
303
+
304
+ let defaultTitle = eventType == "enter" ? "Beacon Entered" : "Beacon Exited"
305
+ let title: String
306
+ if eventType == "enter" {
307
+ title = (eventsCfg?["enterTitle"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? defaultTitle
308
+ } else {
309
+ title = (eventsCfg?["exitTitle"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? defaultTitle
310
+ }
311
+
312
+ let bodyTemplate = (eventsCfg?["body"] as? String).flatMap { $0.isEmpty ? nil : $0 }
313
+ ?? "{identifier} region {event}ed"
314
+ let body = bodyTemplate
315
+ .replacingOccurrences(of: "{identifier}", with: identifier)
316
+ .replacingOccurrences(of: "{event}", with: eventType)
317
+
277
318
  let content = UNMutableNotificationContent()
278
- content.title = eventType == "enter" ? "Beacon Entered" : "Beacon Exited"
279
- content.body = "\(identifier) region \(eventType)ed"
280
- content.sound = .default
319
+ content.title = title
320
+ content.body = body
321
+
322
+ let playSound = eventsCfg?["sound"] as? Bool ?? true
323
+ if playSound { content.sound = .default }
281
324
 
282
325
  let request = UNNotificationRequest(
283
326
  identifier: "beacon_\(eventType)_\(identifier)_\(Date().timeIntervalSince1970)",
@@ -287,6 +330,14 @@ public class ExpoBeaconModule: Module, CLLocationManagerDelegate {
287
330
  UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
288
331
  }
289
332
 
333
+ private func loadNotificationConfig() -> [String: Any] {
334
+ guard let json = UserDefaults.standard.string(forKey: NOTIFICATION_CONFIG_KEY),
335
+ let data = json.data(using: .utf8),
336
+ let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
337
+ else { return [:] }
338
+ return dict
339
+ }
340
+
290
341
  private func constraintMatches(_ a: CLBeaconIdentityConstraint, _ b: CLBeaconIdentityConstraint) -> Bool {
291
342
  return a.uuid == b.uuid && a.major == b.major && a.minor == b.minor
292
343
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Expo module for scanning, pairing, and monitoring iBeacons on Android and iOS",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -46,6 +46,69 @@ export type BeaconDistanceEvent = {
46
46
  distance: number;
47
47
  };
48
48
 
49
+ /** Configuration for beacon enter/exit event notifications. */
50
+ export type BeaconNotificationConfig = {
51
+ /** Whether to show enter/exit notifications. Default: true. */
52
+ enabled?: boolean;
53
+ /** Notification title on beacon enter. Default: "Beacon Entered". */
54
+ enterTitle?: string;
55
+ /** Notification title on beacon exit. Default: "Beacon Exited". */
56
+ exitTitle?: string;
57
+ /**
58
+ * Notification body template. Supports {identifier} and {event} placeholders.
59
+ * Default: "{identifier} region {event}ed".
60
+ */
61
+ body?: string;
62
+ /** Play a sound with the notification (iOS only). Default: true. */
63
+ sound?: boolean;
64
+ /** Android drawable resource name for the notification icon (e.g. "ic_notification"). */
65
+ icon?: string;
66
+ };
67
+
68
+ /** Configuration for the Android foreground service notification (persistent status bar entry). */
69
+ export type ForegroundServiceConfig = {
70
+ /** Title of the persistent notification. Default: "Beacon Monitoring Active". */
71
+ title?: string;
72
+ /** Body text of the persistent notification. Default: "Monitoring for iBeacons in the background". */
73
+ text?: string;
74
+ /** Android drawable resource name for the notification icon. */
75
+ icon?: string;
76
+ };
77
+
78
+ /** Configuration for the Android notification channel. */
79
+ export type NotificationChannelConfig = {
80
+ /** Channel display name shown in system settings. Default: "Beacon Monitoring". */
81
+ name?: string;
82
+ /** Channel description shown in system settings. Default: "Used for background iBeacon region monitoring". */
83
+ description?: string;
84
+ /**
85
+ * Channel importance level. Default: 'low'.
86
+ * Note: Android may ignore decreases in importance after first channel creation until the app is reinstalled.
87
+ */
88
+ importance?: "low" | "default" | "high";
89
+ };
90
+
91
+ /** Combined notification configuration for all notification types. */
92
+ export type NotificationConfig = {
93
+ /** Settings for beacon enter/exit event notifications. */
94
+ beaconEvents?: BeaconNotificationConfig;
95
+ /** Settings for the persistent foreground service notification (Android only). */
96
+ foregroundService?: ForegroundServiceConfig;
97
+ /** Settings for the Android notification channel (Android only). */
98
+ channel?: NotificationChannelConfig;
99
+ };
100
+
101
+ /** Options accepted by startMonitoring(). */
102
+ export type MonitoringOptions = {
103
+ /**
104
+ * Maximum distance in metres for distance-based enter events.
105
+ * Exit events are always emitted when the region is lost.
106
+ */
107
+ maxDistance?: number;
108
+ /** Notification configuration overrides to apply for this monitoring session. */
109
+ notifications?: NotificationConfig;
110
+ };
111
+
49
112
  /** Module event map. */
50
113
  export type ExpoBeaconModuleEvents = {
51
114
  onBeaconEnter: (params: BeaconRegionEvent) => void;
@@ -4,6 +4,8 @@ import {
4
4
  ExpoBeaconModuleEvents,
5
5
  BeaconScanResult,
6
6
  PairedBeacon,
7
+ NotificationConfig,
8
+ MonitoringOptions,
7
9
  } from "./ExpoBeacon.types";
8
10
 
9
11
  declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
@@ -33,15 +35,21 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
33
35
  */
34
36
  getPairedBeacons(): PairedBeacon[];
35
37
 
38
+ /**
39
+ * Set persistent notification configuration. Settings are saved and applied to all
40
+ * subsequent monitoring sessions until explicitly changed.
41
+ */
42
+ setNotificationConfig(config: NotificationConfig): void;
43
+
36
44
  /**
37
45
  * Start background region monitoring for all paired beacons.
38
46
  * On Android starts a foreground service.
39
47
  * On iOS starts CLLocationManager region monitoring.
40
- * @param maxDistance Optional distance threshold in metres. Enter events are only
41
- * emitted when the beacon is measured to be within this distance.
42
- * Exit events are always emitted when the beacon region is lost.
48
+ *
49
+ * Accepts a plain number (backward-compatible maxDistance shorthand) or a
50
+ * MonitoringOptions object with maxDistance and/or notification overrides.
43
51
  */
44
- startMonitoring(maxDistance?: number): Promise<void>;
52
+ startMonitoring(options?: MonitoringOptions | number): Promise<void>;
45
53
 
46
54
  /**
47
55
  * Stop background region monitoring.
@@ -61,4 +69,14 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
61
69
  requestPermissionsAsync(): Promise<boolean>;
62
70
  }
63
71
 
64
- export default requireNativeModule<ExpoBeaconModule>("ExpoBeacon");
72
+ try {
73
+ // eslint-disable-next-line import/no-mutable-exports
74
+ var module = requireNativeModule<ExpoBeaconModule>("ExpoBeacon");
75
+ } catch {
76
+ throw new Error(
77
+ "expo-beacon: native module not found. Make sure you are using a development build " +
78
+ "(not Expo Go) and have run `npx expo prebuild` followed by a native rebuild.",
79
+ );
80
+ }
81
+
82
+ export default module;
package/src/index.ts CHANGED
@@ -7,5 +7,11 @@ export type {
7
7
  PairedBeacon,
8
8
  BeaconRegionEvent,
9
9
  BeaconRangingEvent,
10
+ BeaconDistanceEvent,
10
11
  ExpoBeaconModuleEvents,
12
+ NotificationConfig,
13
+ MonitoringOptions,
14
+ BeaconNotificationConfig,
15
+ ForegroundServiceConfig,
16
+ NotificationChannelConfig,
11
17
  } from "./ExpoBeacon.types";