expo-beacon 0.10.2 → 0.10.4

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
@@ -271,6 +271,8 @@ function BeaconScreen() {
271
271
  | `getMonitoringConfig()` | Read the current monitoring config + active-state snapshot. |
272
272
  | `getMonitoredDeviceState()` / `getMonitoredDeviceStates()` | Native state snapshot for one / all paired devices. |
273
273
  | `setNotificationConfig()` | Persist notification configuration for monitoring sessions. |
274
+ | `setBeaconNotificationConfig()` | Persist only beacon notification settings. |
275
+ | `setCarPlayNotificationConfig()` | Persist only CarPlay / Android Auto notification settings. |
274
276
  | `enableEventLogging()` / `disableEventLogging()` | Toggle SQLite logging (updates `isEventLoggingEnabled`). |
275
277
  | `getEventLogs()` / `clearEventLogs()` / `destroyEventLogs()` | Read / clear / drop the persisted event log. |
276
278
  | `setApiEndpoint()` / `getApiEndpoint()` | Configure / read the native event-forwarding endpoint. |
@@ -660,28 +662,57 @@ await ExpoBeacon.startMonitoring();
660
662
 
661
663
  ```ts
662
664
  ExpoBeacon.setNotificationConfig({
663
- // Enter/exit alert notifications (both platforms)
664
- beaconEvents: {
665
- enabled: true, // Set false to suppress notifications entirely
666
- enterTitle: "Beacon nearby",
667
- exitTitle: "Beacon out of range",
668
- body: "{identifier} {event}ed", // Placeholders: {identifier}, {event}
669
- sound: true, // iOS only
670
- icon: "ic_beacon_notification", // Android only — drawable resource name
671
- },
665
+ beacons: {
666
+ // Enter/exit/timeout alert notifications (both platforms)
667
+ events: {
668
+ enabled: true, // Set false to suppress beacon alerts
669
+ enterTitle: "Beacon nearby",
670
+ exitTitle: "Beacon out of range",
671
+ timeoutTitle: "Beacon timed out",
672
+ body: "{identifier} {event}ed", // Placeholders: {identifier}, {event}
673
+ sound: true, // iOS only
674
+ icon: "ic_beacon_notification", // Android only — drawable resource name
675
+ },
676
+
677
+ // Persistent status-bar notification for beacon monitoring (Android only)
678
+ foregroundService: {
679
+ title: "My App — Beacon monitoring",
680
+ text: "Watching for nearby beacons",
681
+ icon: "ic_service",
682
+ },
672
683
 
673
- // Persistent status-bar notification (Android only)
674
- foregroundService: {
675
- title: "My App — Monitoring",
676
- text: "Watching for nearby beacons",
677
- icon: "ic_service",
684
+ // Android notification channel for beacon notifications
685
+ channel: {
686
+ name: "Proximity Alerts",
687
+ description: "Alerts when beacons enter or leave range",
688
+ importance: "default", // "low" | "default" | "high"
689
+ },
678
690
  },
679
691
 
680
- // Android notification channel
681
- channel: {
682
- name: "Proximity Alerts",
683
- description: "Alerts when beacons enter or leave range",
684
- importance: "default", // "low" | "default" | "high"
692
+ carPlay: {
693
+ // Connect/disconnect notifications (both platforms)
694
+ events: {
695
+ enabled: true, // Set false to suppress CarPlay alerts
696
+ connectedTitle: "CarPlay connected",
697
+ disconnectedTitle: "CarPlay disconnected",
698
+ body: "CarPlay {event} {transport}", // Placeholders: {event}, {transport}
699
+ sound: true, // iOS only
700
+ icon: "ic_carplay_notification", // Android only — drawable resource name
701
+ },
702
+
703
+ // Persistent status-bar notification in CarPlay-only mode (Android only)
704
+ foregroundService: {
705
+ title: "My App — Vehicle monitoring",
706
+ text: "Monitoring CarPlay / Android Auto",
707
+ icon: "ic_service",
708
+ },
709
+
710
+ // Android notification channel for CarPlay notifications
711
+ channel: {
712
+ name: "CarPlay Alerts",
713
+ description: "CarPlay and Android Auto connection notifications",
714
+ importance: "default",
715
+ },
685
716
  },
686
717
  });
687
718
  ```
@@ -1323,10 +1354,28 @@ setNotificationConfig(config: NotificationConfig): void
1323
1354
 
1324
1355
  Persists notification configuration applied to **all subsequent monitoring sessions**. Survives app restarts.
1325
1356
 
1326
- For one-off overrides, pass `notifications` inside `startMonitoring(options)` instead.
1357
+ For one-off overrides, pass `notifications` inside `startMonitoring(options)` instead. Monitoring-time notification overrides are merged into the persisted notification config, so omitted beacon or CarPlay sections are preserved.
1358
+
1359
+ Use the top-level `beacons` and `carPlay` sections to configure those notification streams independently. Legacy keys (`beaconEvents`, `carPlayEvents`, `foregroundService`, `channel`, `carPlayChannel`) are still accepted for existing apps.
1327
1360
 
1328
1361
  See [`NotificationConfig`](#notificationconfig) for the full shape.
1329
1362
 
1363
+ ### `setBeaconNotificationConfig(config)`
1364
+
1365
+ ```ts
1366
+ setBeaconNotificationConfig(config: BeaconNotificationSettings | BeaconNotificationConfig): void
1367
+ ```
1368
+
1369
+ Persists only beacon notification settings without replacing CarPlay settings. Passing a plain `BeaconNotificationConfig` is treated as `beacons.events`.
1370
+
1371
+ ### `setCarPlayNotificationConfig(config)`
1372
+
1373
+ ```ts
1374
+ setCarPlayNotificationConfig(config: CarPlayNotificationSettings | CarPlayNotificationConfig): void
1375
+ ```
1376
+
1377
+ Persists only CarPlay / Android Auto notification settings without replacing beacon settings. Passing a plain `CarPlayNotificationConfig` is treated as `carPlay.events`.
1378
+
1330
1379
  ---
1331
1380
 
1332
1381
  ### `enableEventLogging()`
@@ -1591,9 +1640,13 @@ import type {
1591
1640
  ExpoBeaconModuleEvents,
1592
1641
  MonitoringOptions,
1593
1642
  NotificationConfig,
1643
+ BeaconNotificationSettings,
1594
1644
  BeaconNotificationConfig,
1645
+ CarPlayNotificationSettings,
1646
+ CarPlayNotificationConfig,
1595
1647
  ForegroundServiceConfig,
1596
1648
  NotificationChannelConfig,
1649
+ CarPlayChannelConfig,
1597
1650
  EventLogQueryOptions,
1598
1651
  EventLogEntry,
1599
1652
  } from "expo-beacon";
@@ -1774,9 +1827,35 @@ Top-level notification configuration.
1774
1827
 
1775
1828
  ```ts
1776
1829
  type NotificationConfig = {
1777
- beaconEvents?: BeaconNotificationConfig; // Enter/exit alerts
1778
- foregroundService?: ForegroundServiceConfig; // Android only — persistent status bar
1779
- channel?: NotificationChannelConfig; // Android only — channel settings
1830
+ beacons?: BeaconNotificationSettings;
1831
+ carPlay?: CarPlayNotificationSettings;
1832
+
1833
+ // Legacy aliases, still accepted:
1834
+ beaconEvents?: BeaconNotificationConfig;
1835
+ carPlayEvents?: CarPlayNotificationConfig;
1836
+ foregroundService?: ForegroundServiceConfig;
1837
+ channel?: NotificationChannelConfig;
1838
+ carPlayChannel?: CarPlayChannelConfig;
1839
+ };
1840
+ ```
1841
+
1842
+ ### `BeaconNotificationSettings`
1843
+
1844
+ ```ts
1845
+ type BeaconNotificationSettings = {
1846
+ events?: BeaconNotificationConfig;
1847
+ foregroundService?: ForegroundServiceConfig; // Android only, title/text/icon only
1848
+ channel?: NotificationChannelConfig; // Android only, beacon alert channel
1849
+ };
1850
+ ```
1851
+
1852
+ ### `CarPlayNotificationSettings`
1853
+
1854
+ ```ts
1855
+ type CarPlayNotificationSettings = {
1856
+ events?: CarPlayNotificationConfig;
1857
+ foregroundService?: ForegroundServiceConfig; // Android only, CarPlay-only title/text/icon
1858
+ channel?: CarPlayChannelConfig; // Android only, CarPlay alert channel
1780
1859
  };
1781
1860
  ```
1782
1861
 
@@ -1805,6 +1884,8 @@ type ForegroundServiceConfig = {
1805
1884
  };
1806
1885
  ```
1807
1886
 
1887
+ Foreground service notifications always use the dedicated Android channel `expo_beacon_foreground_channel`, created with low importance and no sound/vibration. This config only changes the notification content.
1888
+
1808
1889
  ### `NotificationChannelConfig`
1809
1890
 
1810
1891
  ```ts
@@ -1815,6 +1896,30 @@ type NotificationChannelConfig = {
1815
1896
  };
1816
1897
  ```
1817
1898
 
1899
+ ### `CarPlayNotificationConfig`
1900
+
1901
+ ```ts
1902
+ type CarPlayNotificationConfig = {
1903
+ enabled?: boolean; // Default: true. Set false to suppress.
1904
+ connectedTitle?: string; // Default: "CarPlay Connected"
1905
+ disconnectedTitle?: string; // Default: "CarPlay Disconnected"
1906
+ body?: string; // Default: "CarPlay session {event}"
1907
+ // Supports {event} and {transport} placeholders.
1908
+ sound?: boolean; // iOS only. Default: true
1909
+ icon?: string; // Android only. Drawable resource name.
1910
+ };
1911
+ ```
1912
+
1913
+ ### `CarPlayChannelConfig`
1914
+
1915
+ ```ts
1916
+ type CarPlayChannelConfig = {
1917
+ name?: string; // Default: "CarPlay / Android Auto"
1918
+ description?: string; // Default: "CarPlay and Android Auto connect/disconnect notifications"
1919
+ importance?: "low" | "default" | "high"; // Default: "default"
1920
+ };
1921
+ ```
1922
+
1818
1923
  ### `BeaconTimeoutEvent`
1819
1924
 
1820
1925
  Payload for `onBeaconTimeout`.
@@ -2101,7 +2206,7 @@ override fun onCreate() {
2101
2206
 
2102
2207
  ## Notifications
2103
2208
 
2104
- A local notification is posted automatically for every beacon enter/exit event (both iBeacon and Eddystone) during monitoring.
2209
+ Local notifications are posted automatically for beacon enter/exit/timeout events and CarPlay / Android Auto connect/disconnect events. Beacon notifications and CarPlay notifications have separate configuration sections and separate Android channels.
2105
2210
 
2106
2211
  ### Default Values
2107
2212
 
@@ -2110,16 +2215,25 @@ A local notification is posted automatically for every beacon enter/exit event (
2110
2215
  | Enter title | `"Beacon Entered"` |
2111
2216
  | Exit title | `"Beacon Exited"` |
2112
2217
  | Body | `"{identifier} region {event}ed"` |
2218
+ | CarPlay connected title | `"CarPlay Connected"` |
2219
+ | CarPlay disconnected title | `"CarPlay Disconnected"` |
2220
+ | CarPlay body | `"CarPlay session {event}"` |
2113
2221
  | Sound (iOS) | `true` |
2114
2222
  | Icon (Android) | System `ic_dialog_info` |
2115
2223
  | Foreground service title | `"Beacon Monitoring Active"` |
2116
2224
  | Foreground service text | `"Monitoring for iBeacons in the background"` |
2225
+ | CarPlay-only foreground title | `"Connected device monitoring active"` |
2226
+ | CarPlay-only foreground text | `"Monitoring connected vehicle (CarPlay/Android Auto)"` |
2227
+ | Foreground channel name (Android) | `"Beacon Foreground Service"` |
2228
+ | Foreground channel importance (Android) | `"low"` (no sound/vibration) |
2117
2229
  | Channel name (Android) | `"Beacon Monitoring"` |
2118
2230
  | Channel importance (Android) | `"low"` |
2231
+ | CarPlay channel name (Android) | `"CarPlay / Android Auto"` |
2232
+ | CarPlay channel importance (Android) | `"default"` |
2119
2233
 
2120
- ### Android Channel
2234
+ ### Android Channels
2121
2235
 
2122
- Both the foreground service and enter/exit alerts share the channel ID `expo_beacon_channel`. The channel is recreated on each `onStartCommand`, so config changes take effect on the next monitoring start.
2236
+ Foreground-service notifications use their own quiet channel ID, `expo_beacon_foreground_channel`, so the persistent status notification does not make sound or vibrate. Beacon event notifications use `expo_beacon_channel`; CarPlay / Android Auto event notifications use `expo_beacon_carplay_channel`. In CarPlay-only mode, the foreground-service notification still uses the quiet foreground channel but reads title/text/icon from `carPlay.foregroundService`.
2123
2237
 
2124
2238
  > **Android channel importance note**: Android prevents decreasing channel importance after the first notification. Increasing works; decreasing has no effect until the user clears notification settings or reinstalls the app.
2125
2239
 
@@ -19,6 +19,7 @@ import org.altbeacon.beacon.*
19
19
  import org.json.JSONArray
20
20
 
21
21
  private const val CHANNEL_ID = "expo_beacon_channel"
22
+ private const val FOREGROUND_CHANNEL_ID = "expo_beacon_foreground_channel"
22
23
  private const val CARPLAY_CHANNEL_ID = "expo_beacon_carplay_channel"
23
24
  internal const val FOREGROUND_NOTIF_ID = 1001
24
25
  /**
@@ -102,6 +103,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
102
103
  override fun onCreate() {
103
104
  super.onCreate()
104
105
  activeService = this
106
+ ensureForegroundNotificationChannel(this)
105
107
  ensureNotificationChannel(this)
106
108
  ensureCarPlayNotificationChannel(this)
107
109
  apiForwarder = BeaconApiForwarder(this)
@@ -947,7 +949,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
947
949
 
948
950
  private fun showEnterExitNotification(region: Region, eventType: String) {
949
951
  val config = readNotificationConfig()
950
- val eventsConfig = config.optJSONObject("beaconEvents")
952
+ val eventsConfig = notificationSection(config, "beacons", "events", "beaconEvents")
951
953
 
952
954
  // Respect the enabled flag (defaults to true)
953
955
  if (eventsConfig != null && !eventsConfig.optBoolean("enabled", true)) return
@@ -1162,7 +1164,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
1162
1164
 
1163
1165
  private fun showCarPlayNotification(eventType: String, transport: String?) {
1164
1166
  val config = readNotificationConfig()
1165
- val eventsConfig = config.optJSONObject("carPlayEvents")
1167
+ val eventsConfig = notificationSection(config, "carPlay", "events", "carPlayEvents")
1166
1168
 
1167
1169
  // Respect the enabled flag (defaults to true)
1168
1170
  if (eventsConfig != null && !eventsConfig.optBoolean("enabled", true)) return
@@ -1239,6 +1241,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
1239
1241
  .edit()
1240
1242
  .putBoolean(MONITORING_ACTIVE_KEY, true)
1241
1243
  .apply()
1244
+ ensureForegroundNotificationChannel(context)
1242
1245
  ensureNotificationChannel(context)
1243
1246
  ensureCarPlayNotificationChannel(context)
1244
1247
  val intent = Intent(context, BeaconForegroundService::class.java)
@@ -1307,6 +1310,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
1307
1310
  */
1308
1311
  fun enableCarPlay(context: Context) {
1309
1312
  setCarPlayEnabled(context, true)
1313
+ ensureForegroundNotificationChannel(context)
1310
1314
  ensureNotificationChannel(context)
1311
1315
  ensureCarPlayNotificationChannel(context)
1312
1316
  // Arm the WorkManager watchdog (15-min periodic safety net) and the
@@ -1468,6 +1472,15 @@ class BeaconForegroundService : Service(), BeaconConsumer {
1468
1472
  }
1469
1473
  }
1470
1474
 
1475
+ internal fun notificationSection(
1476
+ config: org.json.JSONObject,
1477
+ parentKey: String,
1478
+ childKey: String,
1479
+ legacyKey: String
1480
+ ): org.json.JSONObject? =
1481
+ config.optJSONObject(parentKey)?.optJSONObject(childKey)
1482
+ ?: config.optJSONObject(legacyKey)
1483
+
1471
1484
  /**
1472
1485
  * Resolve the configured small-icon drawable from [config], falling back to a stock icon.
1473
1486
  */
@@ -1488,13 +1501,21 @@ class BeaconForegroundService : Service(), BeaconConsumer {
1488
1501
  private fun ensureChannel(
1489
1502
  context: Context,
1490
1503
  channelId: String,
1491
- configKey: String,
1504
+ parentKey: String,
1505
+ childKey: String,
1506
+ legacyKey: String,
1492
1507
  defaultName: String,
1493
1508
  defaultDescription: String,
1494
1509
  fallbackImportance: Int
1495
1510
  ) {
1496
1511
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
1497
- val channelConfig = readNotificationConfig(context).optJSONObject(configKey)
1512
+ val channelConfig =
1513
+ notificationSection(
1514
+ readNotificationConfig(context),
1515
+ parentKey,
1516
+ childKey,
1517
+ legacyKey
1518
+ )
1498
1519
 
1499
1520
  val channelName =
1500
1521
  channelConfig?.optString("name")?.takeIf { it.isNotEmpty() } ?: defaultName
@@ -1528,6 +1549,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
1528
1549
  ensureChannel(
1529
1550
  context,
1530
1551
  CHANNEL_ID,
1552
+ "beacons",
1553
+ "channel",
1531
1554
  "channel",
1532
1555
  "Beacon Monitoring",
1533
1556
  "Used for background iBeacon region monitoring",
@@ -1543,19 +1566,47 @@ class BeaconForegroundService : Service(), BeaconConsumer {
1543
1566
  ensureChannel(
1544
1567
  context,
1545
1568
  CARPLAY_CHANNEL_ID,
1569
+ "carPlay",
1570
+ "channel",
1546
1571
  "carPlayChannel",
1547
1572
  "CarPlay / Android Auto",
1548
1573
  "CarPlay and Android Auto connect/disconnect notifications",
1549
1574
  NotificationManager.IMPORTANCE_DEFAULT
1550
1575
  )
1551
1576
 
1577
+ /**
1578
+ * Ensure the persistent foreground-service notification has a quiet channel of its own.
1579
+ * Event channels can be default/high importance, but the always-on service status should
1580
+ * never make sound or vibrate.
1581
+ */
1582
+ fun ensureForegroundNotificationChannel(context: Context) {
1583
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
1584
+ val notifMgr = context.getSystemService(NotificationManager::class.java)
1585
+ if (notifMgr?.getNotificationChannel(FOREGROUND_CHANNEL_ID) == null) {
1586
+ val channel =
1587
+ NotificationChannel(
1588
+ FOREGROUND_CHANNEL_ID,
1589
+ "Beacon Foreground Service",
1590
+ NotificationManager.IMPORTANCE_LOW
1591
+ )
1592
+ .apply {
1593
+ description =
1594
+ "Persistent status for beacon and CarPlay background monitoring"
1595
+ setSound(null, null)
1596
+ enableVibration(false)
1597
+ }
1598
+ notifMgr?.createNotificationChannel(channel)
1599
+ }
1600
+ }
1601
+
1552
1602
  /**
1553
1603
  * Build the persistent foreground-service notification from any Context. Static so
1554
1604
  * cold-start paths that run before a service instance exists (e.g. [BootReceiver]) read the
1555
1605
  * same persisted config.
1556
1606
  */
1557
1607
  fun buildForegroundNotification(context: Context): Notification {
1558
- val fgConfig = readNotificationConfig(context).optJSONObject("foregroundService")
1608
+ ensureForegroundNotificationChannel(context)
1609
+ val config = readNotificationConfig(context)
1559
1610
 
1560
1611
  // If beacon monitoring is not active and the service is alive only
1561
1612
  // for CarPlay observation, fall back to a generic notification so
@@ -1568,15 +1619,31 @@ class BeaconForegroundService : Service(), BeaconConsumer {
1568
1619
  val defaultText =
1569
1620
  if (carPlayOnly) "Monitoring connected vehicle (CarPlay/Android Auto)"
1570
1621
  else "Monitoring for iBeacons in the background"
1571
-
1622
+ val fgConfig =
1623
+ if (carPlayOnly) {
1624
+ notificationSection(
1625
+ config,
1626
+ "carPlay",
1627
+ "foregroundService",
1628
+ "foregroundService"
1629
+ )
1630
+ } else {
1631
+ notificationSection(
1632
+ config,
1633
+ "beacons",
1634
+ "foregroundService",
1635
+ "foregroundService"
1636
+ )
1637
+ }
1572
1638
  val title = fgConfig?.optString("title")?.takeIf { it.isNotEmpty() } ?: defaultTitle
1573
1639
  val text = fgConfig?.optString("text")?.takeIf { it.isNotEmpty() } ?: defaultText
1574
1640
 
1575
- return NotificationCompat.Builder(context, CHANNEL_ID)
1641
+ return NotificationCompat.Builder(context, FOREGROUND_CHANNEL_ID)
1576
1642
  .setSmallIcon(resolveIconRes(context, fgConfig))
1577
1643
  .setContentTitle(title)
1578
1644
  .setContentText(text)
1579
1645
  .setPriority(NotificationCompat.PRIORITY_LOW)
1646
+ .setSilent(true)
1580
1647
  .setOngoing(true)
1581
1648
  .build()
1582
1649
  }