@blunt-ly/ble-advertiser 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026, The Blunt.ly Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # BLE Advertiser Module
2
+
3
+ A cross-platform BLE (Bluetooth Low Energy) advertiser module for React Native using Expo Modules API. This module allows you to advertise service UUIDs over BLE on both Android and iOS platforms, using minimal platform features to support background processing.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @blunt-ly/ble-advertiser
9
+ ```
10
+
11
+ ## API
12
+
13
+ ### Methods
14
+
15
+ #### `broadcast(uuids: string[]): Promise<string>`
16
+
17
+ Start BLE advertising with the specified service UUIDs.
18
+
19
+ ```typescript
20
+ import * as BleAdvertiser from '@blunt-ly/ble-advertiser';
21
+
22
+ const serviceUUIDs = [
23
+ '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
24
+ '180D' // 16-bit UUIDs are also supported
25
+ ];
26
+
27
+ try {
28
+ const result = await BleAdvertiser.broadcast(serviceUUIDs);
29
+ console.log('Advertising started:', result);
30
+ } catch (error) {
31
+ console.error('Failed to start advertising:', error);
32
+ }
33
+ ```
34
+
35
+ > [!NOTE]
36
+ > When an iOS app advertises in the background, CoreBluetooth moves its service UUIDs out of the standard advertisement field and into an "overflow area" that is only visible to scanners which already know the UUIDs in advance. To work around this, the module exposes a GATT characteristic that holds the device-specific UUIDs, so a scanner that recognizes a shared "network" UUID in the overflow area can connect via GATT to retrieve them. See [Scanning in iOS background mode](#scanning-in-ios-background-mode) for the consumer-side pattern.
37
+
38
+ #### `stopBroadcast(): Promise<{stopped: boolean, stoppedAdvertisers?: number}>`
39
+
40
+ Stop all BLE advertising.
41
+
42
+ ```typescript
43
+ try {
44
+ const result = await BleAdvertiser.stopBroadcast();
45
+ console.log('Advertising stopped:', result);
46
+ } catch (error) {
47
+ console.error('Failed to stop advertising:', error);
48
+ }
49
+ ```
50
+
51
+ #### `isSupported(): Promise<boolean>`
52
+
53
+ Check if BLE advertising is supported on the current device.
54
+
55
+ ```typescript
56
+ const supported = await BleAdvertiser.isSupported();
57
+ console.log('BLE advertising supported:', supported);
58
+ ```
59
+
60
+ #### `isEnabled(): Promise<boolean>`
61
+
62
+ Check if Bluetooth is currently enabled.
63
+
64
+ ```typescript
65
+ const enabled = await BleAdvertiser.isEnabled();
66
+ console.log('Bluetooth enabled:', enabled);
67
+ ```
68
+
69
+ ### Events
70
+
71
+ #### `onAdvertisingStarted`
72
+
73
+ Fired when advertising starts successfully.
74
+
75
+ ```typescript
76
+ import { addAdvertisingStartedListener } from '@blunt-ly/ble-advertiser';
77
+
78
+ const subscription = addAdvertisingStartedListener((event) => {
79
+ console.log('Advertising started with UUIDs:', event.uuids);
80
+ });
81
+
82
+ // Don't forget to remove the listener
83
+ subscription.remove();
84
+ ```
85
+
86
+ #### `onAdvertisingFailed`
87
+
88
+ Fired when advertising fails to start.
89
+
90
+ ```typescript
91
+ import { addAdvertisingFailedListener } from '@blunt-ly/ble-advertiser';
92
+
93
+ const subscription = addAdvertisingFailedListener((event) => {
94
+ console.log('Advertising failed:', event.error);
95
+ if (event.errorCode) {
96
+ console.log('Error code:', event.errorCode);
97
+ }
98
+ });
99
+ ```
100
+
101
+ #### `onAdvertisingStopped`
102
+
103
+ Fired when advertising is stopped.
104
+
105
+ ```typescript
106
+ import { addAdvertisingStoppedListener } from '@blunt-ly/ble-advertiser';
107
+
108
+ const subscription = addAdvertisingStoppedListener((event) => {
109
+ console.log('Advertising stopped');
110
+ if (event.uuids) {
111
+ console.log('UUIDs that were being advertised:', event.uuids);
112
+ }
113
+ });
114
+ ```
115
+
116
+ ## Error Handling
117
+
118
+ The module provides comprehensive error handling with specific error codes and messages:
119
+
120
+ - `BLUETOOTH_NOT_AVAILABLE` - Bluetooth adapter not available
121
+ - `BLUETOOTH_DISABLED` - Bluetooth is turned off
122
+ - `ADVERTISER_NOT_AVAILABLE` - BLE advertiser not supported
123
+ - `INVALID_UUID` - Invalid UUID format provided
124
+ - `NO_UUIDS` - No service UUIDs provided
125
+ - `ADVERTISING_FAILED` - Platform-specific advertising failure
126
+
127
+ ## Example Usage
128
+
129
+ ```typescript
130
+ import React, { useEffect, useState } from 'react';
131
+ import { View, Button, Text, Alert } from 'react-native';
132
+ import * as BleAdvertiser from '@blunt-ly/ble-advertiser';
133
+
134
+ export default function BleAdvertiserExample() {
135
+ const [isAdvertising, setIsAdvertising] = useState(false);
136
+ const [isSupported, setIsSupported] = useState(false);
137
+
138
+ useEffect(() => {
139
+ // Check if BLE advertising is supported
140
+ BleAdvertiser.isSupported().then(setIsSupported);
141
+
142
+ // Set up event listeners
143
+ const startedSubscription = BleAdvertiser.addAdvertisingStartedListener((event) => {
144
+ setIsAdvertising(true);
145
+ Alert.alert('Advertising Started', `UUIDs: ${event.uuids.join(', ')}`);
146
+ });
147
+
148
+ const failedSubscription = BleAdvertiser.addAdvertisingFailedListener((event) => {
149
+ setIsAdvertising(false);
150
+ Alert.alert('Advertising Failed', event.error);
151
+ });
152
+
153
+ const stoppedSubscription = BleAdvertiser.addAdvertisingStoppedListener(() => {
154
+ setIsAdvertising(false);
155
+ Alert.alert('Advertising Stopped');
156
+ });
157
+
158
+ return () => {
159
+ startedSubscription.remove();
160
+ failedSubscription.remove();
161
+ stoppedSubscription.remove();
162
+ };
163
+ }, []);
164
+
165
+ const startAdvertising = async () => {
166
+ try {
167
+ const serviceUUIDs = ['6ba7b810-9dad-11d1-80b4-00c04fd430c8'];
168
+ await BleAdvertiser.broadcast(serviceUUIDs);
169
+ } catch (error) {
170
+ Alert.alert('Error', error.message);
171
+ }
172
+ };
173
+
174
+ const stopAdvertising = async () => {
175
+ try {
176
+ await BleAdvertiser.stopBroadcast();
177
+ } catch (error) {
178
+ Alert.alert('Error', error.message);
179
+ }
180
+ };
181
+
182
+ if (!isSupported) {
183
+ return (
184
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
185
+ <Text>BLE Advertising is not supported on this device</Text>
186
+ </View>
187
+ );
188
+ }
189
+
190
+ return (
191
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
192
+ <Text>BLE Advertising Status: {isAdvertising ? 'Active' : 'Inactive'}</Text>
193
+ <Button
194
+ title={isAdvertising ? 'Stop Advertising' : 'Start Advertising'}
195
+ onPress={isAdvertising ? stopAdvertising : startAdvertising}
196
+ />
197
+ </View>
198
+ );
199
+ }
200
+ ```
201
+
202
+ ## Scanning in iOS background mode
203
+
204
+ When the advertising app is backgrounded on iOS, its device-specific service UUIDs only surface in the scanner's `overflowServiceUUIDs` field — and only if the scanner is already scanning for a known "network" UUID that the advertiser also broadcasts. The pattern is:
205
+
206
+ 1. The advertiser broadcasts a shared `networkId` UUID plus a device-specific UUID.
207
+ 2. The scanner filters on `networkId`.
208
+ 3. In the foreground both UUIDs are visible in `advertising.serviceUUIDs`.
209
+ 4. In the background only `networkId` is visible (via `overflowServiceUUIDs`); the scanner then connects via GATT to read the device-specific UUID from the characteristic exposed by this module.
210
+
211
+ ```typescript
212
+ const NETWORK_ID = '<your-shared-network-uuid>';
213
+ const inFlightConnections = new Set<string>();
214
+
215
+ await scanner.start(async (peripheral) => {
216
+ let targetBroadcastId: string | undefined | null;
217
+
218
+ // Normal path: both service UUIDs are visible in the advertisement
219
+ if (peripheral.advertising.serviceUUIDs?.length === 2) {
220
+ targetBroadcastId = peripheral.advertising.serviceUUIDs.find(
221
+ (uuid) => uuid.toLowerCase() !== NETWORK_ID.toLowerCase()
222
+ );
223
+ }
224
+
225
+ // Overflow path: only the known network UUID is visible via the overflow area.
226
+ // Connect via GATT to read the device-specific broadcast ID.
227
+ if (!targetBroadcastId) {
228
+ const overflowUUIDs = peripheral.advertising.overflowServiceUUIDs as string[] | undefined;
229
+ const hasNetworkInOverflow = overflowUUIDs?.some(
230
+ (uuid) => uuid.toLowerCase() === NETWORK_ID.toLowerCase()
231
+ );
232
+ if (!hasNetworkInOverflow) return;
233
+
234
+ if (inFlightConnections.has(peripheral.id)) return;
235
+ inFlightConnections.add(peripheral.id);
236
+
237
+ targetBroadcastId = await readBroadcastIdViaGatt(peripheral.id, NETWORK_ID);
238
+
239
+ // Allow re-connection after a cooldown
240
+ setTimeout(() => inFlightConnections.delete(peripheral.id), 60_000);
241
+ }
242
+
243
+ if (!targetBroadcastId) return;
244
+
245
+ // Handle the resolved broadcast ID...
246
+ }, [NETWORK_ID]);
247
+ ```
@@ -0,0 +1,43 @@
1
+ apply plugin: 'com.android.library'
2
+
3
+ group = 'expo.modules.bleadvertiser'
4
+ version = '0.0.0'
5
+
6
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
+ apply from: expoModulesCorePlugin
8
+ applyKotlinExpoModulesCorePlugin()
9
+ useCoreDependencies()
10
+ useExpoPublishing()
11
+
12
+ // If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
13
+ // The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
14
+ // Most of the time, you may like to manage the Android SDK versions yourself.
15
+ def useManagedAndroidSdkVersions = false
16
+ if (useManagedAndroidSdkVersions) {
17
+ useDefaultAndroidSdkVersions()
18
+ } else {
19
+ buildscript {
20
+ // Simple helper that allows the root project to override versions declared by this library.
21
+ ext.safeExtGet = { prop, fallback ->
22
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
23
+ }
24
+ }
25
+ project.android {
26
+ compileSdkVersion safeExtGet("compileSdkVersion", 36)
27
+ defaultConfig {
28
+ minSdkVersion safeExtGet("minSdkVersion", 24)
29
+ targetSdkVersion safeExtGet("targetSdkVersion", 36)
30
+ }
31
+ }
32
+ }
33
+
34
+ android {
35
+ namespace "expo.modules.bleadvertiser"
36
+ defaultConfig {
37
+ versionCode 1
38
+ versionName "0.0.0"
39
+ }
40
+ lintOptions {
41
+ abortOnError false
42
+ }
43
+ }
@@ -0,0 +1,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,173 @@
1
+ package expo.modules.bleadvertiser
2
+
3
+ import android.bluetooth.BluetoothAdapter
4
+ import android.bluetooth.BluetoothManager
5
+ import android.bluetooth.le.AdvertiseCallback
6
+ import android.bluetooth.le.AdvertiseData
7
+ import android.bluetooth.le.AdvertiseSettings
8
+ import android.bluetooth.le.BluetoothLeAdvertiser
9
+ import android.content.Context
10
+ import android.os.ParcelUuid
11
+ import android.util.Log
12
+ import expo.modules.kotlin.Promise
13
+ import expo.modules.kotlin.modules.Module
14
+ import expo.modules.kotlin.modules.ModuleDefinition
15
+ import java.util.UUID
16
+
17
+ class BleAdvertiserModule : Module() {
18
+ private var bluetoothAdapter: BluetoothAdapter? = null
19
+ private val advertisers = mutableMapOf<String, BluetoothLeAdvertiser>()
20
+ private val advertiseCallbacks = mutableMapOf<String, AdvertiseCallback>()
21
+
22
+ override fun definition() = ModuleDefinition {
23
+ Name("BleAdvertiser")
24
+
25
+ Events("onAdvertisingStarted", "onAdvertisingFailed", "onAdvertisingStopped")
26
+
27
+ OnCreate {
28
+ val bluetoothManager = appContext.reactContext?.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
29
+ bluetoothAdapter = bluetoothManager?.adapter
30
+ }
31
+
32
+ AsyncFunction("broadcast") { uuids: List<String>, promise: Promise ->
33
+ try {
34
+ if (bluetoothAdapter == null) {
35
+ promise.reject("BLUETOOTH_NOT_AVAILABLE", "Bluetooth adapter is not available", null)
36
+ return@AsyncFunction
37
+ }
38
+
39
+ if (bluetoothAdapter?.isEnabled != true) {
40
+ promise.reject("BLUETOOTH_DISABLED", "Bluetooth is not enabled", null)
41
+ return@AsyncFunction
42
+ }
43
+
44
+ val advertiser = bluetoothAdapter?.bluetoothLeAdvertiser
45
+ if (advertiser == null) {
46
+ promise.reject("ADVERTISER_NOT_AVAILABLE", "BLE advertiser is not available on this device", null)
47
+ return@AsyncFunction
48
+ }
49
+
50
+ // Convert string UUIDs to ParcelUuid objects
51
+ val serviceUuids = mutableListOf<ParcelUuid>()
52
+ for (uuidString in uuids) {
53
+ try {
54
+ val uuid = UUID.fromString(uuidString)
55
+ serviceUuids.add(ParcelUuid(uuid))
56
+ } catch (e: IllegalArgumentException) {
57
+ promise.reject("INVALID_UUID", "Invalid UUID format: $uuidString", e)
58
+ return@AsyncFunction
59
+ }
60
+ }
61
+
62
+ if (serviceUuids.isEmpty()) {
63
+ promise.reject("NO_UUIDS", "At least one service UUID is required", null)
64
+ return@AsyncFunction
65
+ }
66
+
67
+ // Build advertise settings
68
+ val settings = AdvertiseSettings.Builder()
69
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
70
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
71
+ .setConnectable(false)
72
+ .build()
73
+
74
+ // Build advertise data with service UUIDs
75
+ val dataBuilder = AdvertiseData.Builder()
76
+ .setIncludeDeviceName(false)
77
+ .setIncludeTxPowerLevel(true)
78
+
79
+ // Add all service UUIDs
80
+ for (serviceUuid in serviceUuids) {
81
+ dataBuilder.addServiceUuid(serviceUuid)
82
+ }
83
+
84
+ val advertiseData = dataBuilder.build()
85
+
86
+ val callback = object : AdvertiseCallback() {
87
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
88
+ Log.d("BleAdvertiser", "Advertising started successfully")
89
+ sendEvent("onAdvertisingStarted", mapOf("uuids" to uuids))
90
+ promise.resolve("Advertising started successfully")
91
+ }
92
+
93
+ override fun onStartFailure(errorCode: Int) {
94
+ val errorMessage = when (errorCode) {
95
+ ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> "Advertising feature is not supported"
96
+ ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> "Too many advertisers"
97
+ ADVERTISE_FAILED_ALREADY_STARTED -> "Advertising already started"
98
+ ADVERTISE_FAILED_DATA_TOO_LARGE -> "Advertise data too large"
99
+ ADVERTISE_FAILED_INTERNAL_ERROR -> "Internal error"
100
+ else -> "Unknown error code: $errorCode"
101
+ }
102
+ Log.e("BleAdvertiser", "Advertising failed: $errorMessage")
103
+ sendEvent("onAdvertisingFailed", mapOf("error" to errorMessage, "errorCode" to errorCode))
104
+ promise.reject("ADVERTISING_FAILED", errorMessage, null)
105
+ }
106
+ }
107
+
108
+ // Generate a key for this advertiser instance
109
+ val key = uuids.sorted().joinToString(",")
110
+
111
+ // Stop any existing advertiser with the same UUIDs
112
+ advertisers[key]?.let { existingAdvertiser ->
113
+ advertiseCallbacks[key]?.let { existingCallback ->
114
+ existingAdvertiser.stopAdvertising(existingCallback)
115
+ }
116
+ }
117
+
118
+ // Store the new advertiser and callback
119
+ advertisers[key] = advertiser
120
+ advertiseCallbacks[key] = callback
121
+
122
+ // Start advertising
123
+ advertiser.startAdvertising(settings, advertiseData, callback)
124
+
125
+ } catch (e: Exception) {
126
+ Log.e("BleAdvertiser", "Error starting advertising", e)
127
+ promise.reject("UNEXPECTED_ERROR", e.message ?: "Unknown error", e)
128
+ }
129
+ }
130
+
131
+ AsyncFunction("stopBroadcast") { promise: Promise ->
132
+ try {
133
+ val stoppedKeys = mutableListOf<String>()
134
+
135
+ for ((key, advertiser) in advertisers) {
136
+ advertiseCallbacks[key]?.let { callback ->
137
+ advertiser.stopAdvertising(callback)
138
+ stoppedKeys.add(key)
139
+ }
140
+ }
141
+
142
+ advertisers.clear()
143
+ advertiseCallbacks.clear()
144
+
145
+ sendEvent("onAdvertisingStopped", mapOf("stoppedCount" to stoppedKeys.size))
146
+ promise.resolve(mapOf("stoppedAdvertisers" to stoppedKeys.size))
147
+
148
+ } catch (e: Exception) {
149
+ Log.e("BleAdvertiser", "Error stopping advertising", e)
150
+ promise.reject("STOP_ERROR", e.message ?: "Unknown error", e)
151
+ }
152
+ }
153
+
154
+ AsyncFunction("isSupported") { promise: Promise ->
155
+ try {
156
+ val isSupported = bluetoothAdapter != null &&
157
+ bluetoothAdapter?.bluetoothLeAdvertiser != null
158
+ promise.resolve(isSupported)
159
+ } catch (e: Exception) {
160
+ promise.reject("CHECK_SUPPORT_ERROR", e.message ?: "Unknown error", e)
161
+ }
162
+ }
163
+
164
+ AsyncFunction("isEnabled") { promise: Promise ->
165
+ try {
166
+ val isEnabled = bluetoothAdapter?.isEnabled == true
167
+ promise.resolve(isEnabled)
168
+ } catch (e: Exception) {
169
+ promise.reject("CHECK_ENABLED_ERROR", e.message ?: "Unknown error", e)
170
+ }
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,18 @@
1
+ export type AdvertisingStartedEvent = {
2
+ uuids: string[];
3
+ };
4
+ export type AdvertisingFailedEvent = {
5
+ error: string;
6
+ errorCode?: number;
7
+ uuids?: string[];
8
+ };
9
+ export type AdvertisingStoppedEvent = {
10
+ uuids?: string[];
11
+ stoppedCount?: number;
12
+ };
13
+ export type BleAdvertiserModuleEvents = {
14
+ onAdvertisingStarted: (params: AdvertisingStartedEvent) => void;
15
+ onAdvertisingFailed: (params: AdvertisingFailedEvent) => void;
16
+ onAdvertisingStopped: (params: AdvertisingStoppedEvent) => void;
17
+ };
18
+ //# sourceMappingURL=BleAdvertiser.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BleAdvertiser.types.d.ts","sourceRoot":"","sources":["../src/BleAdvertiser.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,oBAAoB,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAChE,mBAAmB,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;IAC9D,oBAAoB,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;CACjE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=BleAdvertiser.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BleAdvertiser.types.js","sourceRoot":"","sources":["../src/BleAdvertiser.types.ts"],"names":[],"mappings":"","sourcesContent":["export type AdvertisingStartedEvent = {\n uuids: string[];\n};\n\nexport type AdvertisingFailedEvent = {\n error: string;\n errorCode?: number;\n uuids?: string[];\n};\n\nexport type AdvertisingStoppedEvent = {\n uuids?: string[];\n stoppedCount?: number;\n};\n\nexport type BleAdvertiserModuleEvents = {\n onAdvertisingStarted: (params: AdvertisingStartedEvent) => void;\n onAdvertisingFailed: (params: AdvertisingFailedEvent) => void;\n onAdvertisingStopped: (params: AdvertisingStoppedEvent) => void;\n};\n"]}
@@ -0,0 +1,31 @@
1
+ import { NativeModule } from 'expo';
2
+ import { BleAdvertiserModuleEvents } from './BleAdvertiser.types';
3
+ declare class BleAdvertiserModule extends NativeModule<BleAdvertiserModuleEvents> {
4
+ /**
5
+ * Start BLE advertising with the specified service UUIDs
6
+ * @param uuids Array of service UUID strings to advertise
7
+ * @returns Promise that resolves when advertising starts successfully
8
+ */
9
+ broadcast(uuids: string[]): Promise<string>;
10
+ /**
11
+ * Stop all BLE advertising
12
+ * @returns Promise that resolves with information about stopped advertisers
13
+ */
14
+ stopBroadcast(): Promise<{
15
+ stopped: boolean;
16
+ stoppedAdvertisers?: number;
17
+ }>;
18
+ /**
19
+ * Check if BLE advertising is supported on this device
20
+ * @returns Promise that resolves to true if supported, false otherwise
21
+ */
22
+ isSupported(): Promise<boolean>;
23
+ /**
24
+ * Check if Bluetooth is currently enabled
25
+ * @returns Promise that resolves to true if enabled, false otherwise
26
+ */
27
+ isEnabled(): Promise<boolean>;
28
+ }
29
+ declare const _default: BleAdvertiserModule;
30
+ export default _default;
31
+ //# sourceMappingURL=BleAdvertiserModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BleAdvertiserModule.d.ts","sourceRoot":"","sources":["../src/BleAdvertiserModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAElE,OAAO,OAAO,mBAAoB,SAAQ,YAAY,CAAC,yBAAyB,CAAC;IAC/E;;;;OAIG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAE3C;;;OAGG;IACH,aAAa,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE3E;;;OAGG;IACH,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAE/B;;;OAGG;IACH,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAC9B;;AAGD,wBAAyE"}
@@ -0,0 +1,4 @@
1
+ import { requireNativeModule } from 'expo';
2
+ // This call loads the native module object from the JSI.
3
+ export default requireNativeModule('BleAdvertiser');
4
+ //# sourceMappingURL=BleAdvertiserModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BleAdvertiserModule.js","sourceRoot":"","sources":["../src/BleAdvertiserModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AA+BzD,yDAAyD;AACzD,eAAe,mBAAmB,CAAsB,eAAe,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\nimport { BleAdvertiserModuleEvents } from './BleAdvertiser.types';\n\ndeclare class BleAdvertiserModule extends NativeModule<BleAdvertiserModuleEvents> {\n /**\n * Start BLE advertising with the specified service UUIDs\n * @param uuids Array of service UUID strings to advertise\n * @returns Promise that resolves when advertising starts successfully\n */\n broadcast(uuids: string[]): Promise<string>;\n\n /**\n * Stop all BLE advertising\n * @returns Promise that resolves with information about stopped advertisers\n */\n stopBroadcast(): Promise<{ stopped: boolean; stoppedAdvertisers?: number }>;\n\n /**\n * Check if BLE advertising is supported on this device\n * @returns Promise that resolves to true if supported, false otherwise\n */\n isSupported(): Promise<boolean>;\n\n /**\n * Check if Bluetooth is currently enabled\n * @returns Promise that resolves to true if enabled, false otherwise\n */\n isEnabled(): Promise<boolean>;\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<BleAdvertiserModule>('BleAdvertiser');\n"]}
@@ -0,0 +1,46 @@
1
+ import { EventSubscription } from 'expo-modules-core';
2
+ import { AdvertisingStartedEvent, AdvertisingFailedEvent, AdvertisingStoppedEvent } from './BleAdvertiser.types';
3
+ /**
4
+ * Start BLE advertising with the specified service UUIDs
5
+ * @param uuids Array of service UUID strings to advertise
6
+ * @returns Promise that resolves when advertising starts successfully
7
+ */
8
+ export declare function broadcast(uuids: string[]): Promise<string>;
9
+ /**
10
+ * Stop all BLE advertising
11
+ * @returns Promise that resolves with information about stopped advertisers
12
+ */
13
+ export declare function stopBroadcast(): Promise<{
14
+ stopped: boolean;
15
+ stoppedAdvertisers?: number;
16
+ }>;
17
+ /**
18
+ * Check if BLE advertising is supported on this device
19
+ * @returns Promise that resolves to true if supported, false otherwise
20
+ */
21
+ export declare function isSupported(): Promise<boolean>;
22
+ /**
23
+ * Check if Bluetooth is currently enabled
24
+ * @returns Promise that resolves to true if enabled, false otherwise
25
+ */
26
+ export declare function isEnabled(): Promise<boolean>;
27
+ /**
28
+ * Add listener for advertising started events
29
+ * @param listener Function to call when advertising starts
30
+ * @returns EventSubscription that can be used to remove the listener
31
+ */
32
+ export declare function addAdvertisingStartedListener(listener: (event: AdvertisingStartedEvent) => void): EventSubscription;
33
+ /**
34
+ * Add listener for advertising failed events
35
+ * @param listener Function to call when advertising fails
36
+ * @returns EventSubscription that can be used to remove the listener
37
+ */
38
+ export declare function addAdvertisingFailedListener(listener: (event: AdvertisingFailedEvent) => void): EventSubscription;
39
+ /**
40
+ * Add listener for advertising stopped events
41
+ * @param listener Function to call when advertising stops
42
+ * @returns EventSubscription that can be used to remove the listener
43
+ */
44
+ export declare function addAdvertisingStoppedListener(listener: (event: AdvertisingStoppedEvent) => void): EventSubscription;
45
+ export * from './BleAdvertiser.types';
46
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,uBAAuB,CAAC;AAe/B;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1D;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAE1F;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAE9C;AAED;;;GAGG;AACH,wBAAgB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAE5C;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,GACjD,iBAAiB,CAEnB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,GAChD,iBAAiB,CAEnB;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,GACjD,iBAAiB,CAEnB;AAGD,cAAc,uBAAuB,CAAC"}
package/build/index.js ADDED
@@ -0,0 +1,70 @@
1
+ import { Platform } from 'react-native';
2
+ import BleAdvertiserModule from './BleAdvertiserModule';
3
+ import { expand16BitUuid } from './utils';
4
+ // HACK: Android's BluetoothLeAdvertiser only accepts 128-bit service UUIDs, while
5
+ // CoreBluetooth on iOS accepts the 16-bit short form directly. Expand any
6
+ // 16-bit input before handing it to the Android native module.
7
+ function normalizeUuidsForPlatform(uuids) {
8
+ if (Platform.OS !== 'android')
9
+ return uuids;
10
+ return uuids.map((uuid) => {
11
+ const hex = uuid.replace(/[^0-9a-fA-F]/g, '');
12
+ return hex.length === 4 ? expand16BitUuid(uuid) : uuid;
13
+ });
14
+ }
15
+ /**
16
+ * Start BLE advertising with the specified service UUIDs
17
+ * @param uuids Array of service UUID strings to advertise
18
+ * @returns Promise that resolves when advertising starts successfully
19
+ */
20
+ export function broadcast(uuids) {
21
+ return BleAdvertiserModule.broadcast(normalizeUuidsForPlatform(uuids));
22
+ }
23
+ /**
24
+ * Stop all BLE advertising
25
+ * @returns Promise that resolves with information about stopped advertisers
26
+ */
27
+ export function stopBroadcast() {
28
+ return BleAdvertiserModule.stopBroadcast();
29
+ }
30
+ /**
31
+ * Check if BLE advertising is supported on this device
32
+ * @returns Promise that resolves to true if supported, false otherwise
33
+ */
34
+ export function isSupported() {
35
+ return BleAdvertiserModule.isSupported();
36
+ }
37
+ /**
38
+ * Check if Bluetooth is currently enabled
39
+ * @returns Promise that resolves to true if enabled, false otherwise
40
+ */
41
+ export function isEnabled() {
42
+ return BleAdvertiserModule.isEnabled();
43
+ }
44
+ /**
45
+ * Add listener for advertising started events
46
+ * @param listener Function to call when advertising starts
47
+ * @returns EventSubscription that can be used to remove the listener
48
+ */
49
+ export function addAdvertisingStartedListener(listener) {
50
+ return BleAdvertiserModule.addListener('onAdvertisingStarted', listener);
51
+ }
52
+ /**
53
+ * Add listener for advertising failed events
54
+ * @param listener Function to call when advertising fails
55
+ * @returns EventSubscription that can be used to remove the listener
56
+ */
57
+ export function addAdvertisingFailedListener(listener) {
58
+ return BleAdvertiserModule.addListener('onAdvertisingFailed', listener);
59
+ }
60
+ /**
61
+ * Add listener for advertising stopped events
62
+ * @param listener Function to call when advertising stops
63
+ * @returns EventSubscription that can be used to remove the listener
64
+ */
65
+ export function addAdvertisingStoppedListener(listener) {
66
+ return BleAdvertiserModule.addListener('onAdvertisingStopped', listener);
67
+ }
68
+ // Export the types for consumers
69
+ export * from './BleAdvertiser.types';
70
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAOxC,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1C,kFAAkF;AAClF,0EAA0E;AAC1E,+DAA+D;AAC/D,SAAS,yBAAyB,CAAC,KAAe;IAChD,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;QAC9C,OAAO,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACzD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,KAAe;IACvC,OAAO,mBAAmB,CAAC,SAAS,CAAC,yBAAyB,CAAC,KAAK,CAAC,CAAC,CAAC;AACzE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,mBAAmB,CAAC,aAAa,EAAE,CAAC;AAC7C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,mBAAmB,CAAC,WAAW,EAAE,CAAC;AAC3C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO,mBAAmB,CAAC,SAAS,EAAE,CAAC;AACzC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAC3C,QAAkD;IAElD,OAAO,mBAAmB,CAAC,WAAW,CAAC,sBAAsB,EAAE,QAAQ,CAAC,CAAC;AAC3E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAC1C,QAAiD;IAEjD,OAAO,mBAAmB,CAAC,WAAW,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAAC;AAC1E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAC3C,QAAkD;IAElD,OAAO,mBAAmB,CAAC,WAAW,CAAC,sBAAsB,EAAE,QAAQ,CAAC,CAAC;AAC3E,CAAC;AAED,iCAAiC;AACjC,cAAc,uBAAuB,CAAC","sourcesContent":["import { EventSubscription } from 'expo-modules-core';\nimport { Platform } from 'react-native';\n\nimport {\n AdvertisingStartedEvent,\n AdvertisingFailedEvent,\n AdvertisingStoppedEvent,\n} from './BleAdvertiser.types';\nimport BleAdvertiserModule from './BleAdvertiserModule';\nimport { expand16BitUuid } from './utils';\n\n// HACK: Android's BluetoothLeAdvertiser only accepts 128-bit service UUIDs, while\n// CoreBluetooth on iOS accepts the 16-bit short form directly. Expand any\n// 16-bit input before handing it to the Android native module.\nfunction normalizeUuidsForPlatform(uuids: string[]): string[] {\n if (Platform.OS !== 'android') return uuids;\n return uuids.map((uuid) => {\n const hex = uuid.replace(/[^0-9a-fA-F]/g, '');\n return hex.length === 4 ? expand16BitUuid(uuid) : uuid;\n });\n}\n\n/**\n * Start BLE advertising with the specified service UUIDs\n * @param uuids Array of service UUID strings to advertise\n * @returns Promise that resolves when advertising starts successfully\n */\nexport function broadcast(uuids: string[]): Promise<string> {\n return BleAdvertiserModule.broadcast(normalizeUuidsForPlatform(uuids));\n}\n\n/**\n * Stop all BLE advertising\n * @returns Promise that resolves with information about stopped advertisers\n */\nexport function stopBroadcast(): Promise<{ stopped: boolean; stoppedAdvertisers?: number }> {\n return BleAdvertiserModule.stopBroadcast();\n}\n\n/**\n * Check if BLE advertising is supported on this device\n * @returns Promise that resolves to true if supported, false otherwise\n */\nexport function isSupported(): Promise<boolean> {\n return BleAdvertiserModule.isSupported();\n}\n\n/**\n * Check if Bluetooth is currently enabled\n * @returns Promise that resolves to true if enabled, false otherwise\n */\nexport function isEnabled(): Promise<boolean> {\n return BleAdvertiserModule.isEnabled();\n}\n\n/**\n * Add listener for advertising started events\n * @param listener Function to call when advertising starts\n * @returns EventSubscription that can be used to remove the listener\n */\nexport function addAdvertisingStartedListener(\n listener: (event: AdvertisingStartedEvent) => void\n): EventSubscription {\n return BleAdvertiserModule.addListener('onAdvertisingStarted', listener);\n}\n\n/**\n * Add listener for advertising failed events\n * @param listener Function to call when advertising fails\n * @returns EventSubscription that can be used to remove the listener\n */\nexport function addAdvertisingFailedListener(\n listener: (event: AdvertisingFailedEvent) => void\n): EventSubscription {\n return BleAdvertiserModule.addListener('onAdvertisingFailed', listener);\n}\n\n/**\n * Add listener for advertising stopped events\n * @param listener Function to call when advertising stops\n * @returns EventSubscription that can be used to remove the listener\n */\nexport function addAdvertisingStoppedListener(\n listener: (event: AdvertisingStoppedEvent) => void\n): EventSubscription {\n return BleAdvertiserModule.addListener('onAdvertisingStopped', listener);\n}\n\n// Export the types for consumers\nexport * from './BleAdvertiser.types';\n"]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Expands a 16-bit UUID to the full 128-bit Bluetooth UUID format.
3
+ *
4
+ * @param shortUuid 16-bit UUID string (with or without dashes)
5
+ * @returns Expanded 128-bit UUID string
6
+ * @throws Error if the input is not a valid 16-bit UUID
7
+ */
8
+ export declare function expand16BitUuid(shortUuid: string): string;
9
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAazD"}
package/build/utils.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Expands a 16-bit UUID to the full 128-bit Bluetooth UUID format.
3
+ *
4
+ * @param shortUuid 16-bit UUID string (with or without dashes)
5
+ * @returns Expanded 128-bit UUID string
6
+ * @throws Error if the input is not a valid 16-bit UUID
7
+ */
8
+ export function expand16BitUuid(shortUuid) {
9
+ // Clean and validate input
10
+ const cleaned = shortUuid.replace(/[^0-9a-fA-F]/g, '');
11
+ if (!/^[0-9a-fA-F]{4}$/i.test(cleaned)) {
12
+ throw new Error(`Invalid 16-bit UUID: ${shortUuid}`);
13
+ }
14
+ // Pad to 4 chars and convert to lowercase
15
+ const paddedUuid = cleaned.padStart(4, '0').toLowerCase();
16
+ // Bluetooth Base UUID: 00000000-0000-1000-8000-00805F9B34FB
17
+ return `0000${paddedUuid}-0000-1000-8000-00805f9b34fb`;
18
+ }
19
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,2BAA2B;IAC3B,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IAEvD,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,wBAAwB,SAAS,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,0CAA0C;IAC1C,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAE1D,4DAA4D;IAC5D,OAAO,OAAO,UAAU,8BAA8B,CAAC;AACzD,CAAC","sourcesContent":["/**\n * Expands a 16-bit UUID to the full 128-bit Bluetooth UUID format.\n *\n * @param shortUuid 16-bit UUID string (with or without dashes)\n * @returns Expanded 128-bit UUID string\n * @throws Error if the input is not a valid 16-bit UUID\n */\nexport function expand16BitUuid(shortUuid: string): string {\n // Clean and validate input\n const cleaned = shortUuid.replace(/[^0-9a-fA-F]/g, '');\n\n if (!/^[0-9a-fA-F]{4}$/i.test(cleaned)) {\n throw new Error(`Invalid 16-bit UUID: ${shortUuid}`);\n }\n\n // Pad to 4 chars and convert to lowercase\n const paddedUuid = cleaned.padStart(4, '0').toLowerCase();\n\n // Bluetooth Base UUID: 00000000-0000-1000-8000-00805F9B34FB\n return `0000${paddedUuid}-0000-1000-8000-00805f9b34fb`;\n}\n"]}
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android"],
3
+ "apple": {
4
+ "modules": ["BleAdvertiserModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.bleadvertiser.BleAdvertiserModule"]
8
+ }
9
+ }
@@ -0,0 +1,22 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'BleAdvertiser'
3
+ s.version = '0.0.0'
4
+ s.summary = 'Minimal BLE advertiser for React-Native, optimized for background processing.'
5
+ s.author = 'The Blunt.ly Authors'
6
+ s.homepage = 'https://github.com/Blunt-ly/ble-advertiser'
7
+ s.platforms = {
8
+ :ios => '15.1',
9
+ :tvos => '15.1'
10
+ }
11
+ s.source = { git: '' }
12
+ s.static_framework = true
13
+
14
+ s.dependency 'ExpoModulesCore'
15
+
16
+ # Swift/Objective-C compatibility
17
+ s.pod_target_xcconfig = {
18
+ 'DEFINES_MODULE' => 'YES',
19
+ }
20
+
21
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
22
+ end
@@ -0,0 +1,246 @@
1
+ import ExpoModulesCore
2
+ import CoreBluetooth
3
+
4
+ // The GATT characteristic UUID reuses the network UUID (first element passed to broadcast()).
5
+ // CoreBluetooth distinguishes services and characteristics by their role in the GATT hierarchy.
6
+
7
+ private class BlePeripheralManagerDelegate: NSObject, CBPeripheralManagerDelegate {
8
+ weak var module: BleAdvertiserModule?
9
+
10
+ init(module: BleAdvertiserModule) {
11
+ self.module = module
12
+ super.init()
13
+ }
14
+
15
+ func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
16
+ module?.handlePeripheralManagerDidUpdateState(peripheral)
17
+ }
18
+
19
+ func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
20
+ module?.handlePeripheralManagerDidStartAdvertising(peripheral, error: error)
21
+ }
22
+
23
+ func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
24
+ module?.handlePeripheralManagerDidAddService(peripheral, service: service, error: error)
25
+ }
26
+
27
+ func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
28
+ module?.handlePeripheralManagerDidReceiveRead(peripheral, request: request)
29
+ }
30
+ }
31
+
32
+ public class BleAdvertiserModule: Module {
33
+ private var peripheralManager: CBPeripheralManager?
34
+ private var delegate: BlePeripheralManagerDelegate?
35
+ private var isAdvertising = false
36
+ private var currentAdvertisingUUIDs: [String] = []
37
+ private var gattService: CBMutableService?
38
+ private var broadcastIdCharacteristic: CBMutableCharacteristic?
39
+ private var currentBroadcastId: String?
40
+
41
+ public func definition() -> ModuleDefinition {
42
+ Name("BleAdvertiser")
43
+
44
+ Events("onAdvertisingStarted", "onAdvertisingFailed", "onAdvertisingStopped")
45
+
46
+ OnCreate {
47
+ self.delegate = BlePeripheralManagerDelegate(module: self)
48
+ self.peripheralManager = CBPeripheralManager(delegate: self.delegate, queue: nil)
49
+ }
50
+
51
+ AsyncFunction("broadcast") { (uuids: [String], promise: Promise) in
52
+ guard let peripheralManager = self.peripheralManager else {
53
+ promise.reject("PERIPHERAL_MANAGER_NOT_AVAILABLE", "Peripheral manager is not available")
54
+ return
55
+ }
56
+
57
+ guard peripheralManager.state == .poweredOn else {
58
+ let stateDescription = self.bluetoothStateDescription(peripheralManager.state)
59
+ promise.reject("BLUETOOTH_NOT_READY", "Bluetooth state: \(stateDescription)")
60
+ return
61
+ }
62
+
63
+ guard !uuids.isEmpty else {
64
+ promise.reject("NO_UUIDS", "At least one service UUID is required")
65
+ return
66
+ }
67
+
68
+ // Stop any existing advertising and remove existing GATT services
69
+ if self.isAdvertising {
70
+ peripheralManager.stopAdvertising()
71
+ self.isAdvertising = false
72
+ }
73
+ if self.gattService != nil {
74
+ peripheralManager.removeAllServices()
75
+ self.gattService = nil
76
+ }
77
+
78
+ // Convert string UUIDs to CBUUID objects
79
+ var serviceUUIDs: [CBUUID] = []
80
+ for uuidString in uuids {
81
+ let cbuuid = CBUUID(string: uuidString)
82
+ serviceUUIDs.append(cbuuid)
83
+ }
84
+
85
+ self.currentAdvertisingUUIDs = uuids
86
+
87
+ // Identify the network UUID (first) and broadcast ID (second) from the uuids array.
88
+ // Register a GATT service under the network UUID with a readable characteristic
89
+ // that returns the broadcast ID. This allows centrals that discover us via the
90
+ // overflow area (where only the known network UUID is visible) to connect and
91
+ // retrieve our device-specific broadcast ID through GATT.
92
+ if uuids.count >= 2 {
93
+ let networkUUID = CBUUID(string: uuids[0])
94
+ self.currentBroadcastId = uuids[1]
95
+
96
+ self.broadcastIdCharacteristic = CBMutableCharacteristic(
97
+ type: networkUUID,
98
+ properties: [.read],
99
+ value: nil, // nil = dynamic, served via didReceiveRead delegate
100
+ permissions: [.readable]
101
+ )
102
+
103
+ let service = CBMutableService(type: networkUUID, primary: true)
104
+ service.characteristics = [self.broadcastIdCharacteristic!]
105
+ self.gattService = service
106
+
107
+ // Adding the service triggers didAdd delegate; advertising starts after that
108
+ self.broadcastPromise = promise
109
+ peripheralManager.add(service)
110
+ } else {
111
+ // Single UUID — no GATT service needed, just advertise directly
112
+ let advertisementData: [String: Any] = [CBAdvertisementDataServiceUUIDsKey: serviceUUIDs]
113
+ peripheralManager.startAdvertising(advertisementData)
114
+ self.broadcastPromise = promise
115
+ }
116
+ }
117
+
118
+ AsyncFunction("stopBroadcast") { (promise: Promise) in
119
+ guard let peripheralManager = self.peripheralManager else {
120
+ promise.reject("PERIPHERAL_MANAGER_NOT_AVAILABLE", "Peripheral manager is not available")
121
+ return
122
+ }
123
+
124
+ if self.isAdvertising {
125
+ peripheralManager.stopAdvertising()
126
+ self.isAdvertising = false
127
+ self.sendEvent("onAdvertisingStopped", ["uuids": self.currentAdvertisingUUIDs])
128
+ self.currentAdvertisingUUIDs = []
129
+ }
130
+
131
+ if self.gattService != nil {
132
+ peripheralManager.removeAllServices()
133
+ self.gattService = nil
134
+ self.broadcastIdCharacteristic = nil
135
+ self.currentBroadcastId = nil
136
+ }
137
+
138
+ promise.resolve(["stopped": self.isAdvertising == false])
139
+ }
140
+
141
+ AsyncFunction("isSupported") { (promise: Promise) in
142
+ let isSupported = CBPeripheralManager.authorization == .allowedAlways ||
143
+ CBPeripheralManager.authorization == .notDetermined
144
+ promise.resolve(isSupported)
145
+ }
146
+
147
+ AsyncFunction("isEnabled") { (promise: Promise) in
148
+ guard let peripheralManager = self.peripheralManager else {
149
+ promise.resolve(false)
150
+ return
151
+ }
152
+
153
+ let isEnabled = peripheralManager.state == .poweredOn
154
+ promise.resolve(isEnabled)
155
+ }
156
+ }
157
+
158
+ private var broadcastPromise: Promise?
159
+
160
+ // MARK: - Delegate Handler Methods
161
+
162
+ func handlePeripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
163
+ let stateDescription = bluetoothStateDescription(peripheral.state)
164
+ print("BleAdvertiser: Peripheral manager state changed to \(stateDescription)")
165
+ }
166
+
167
+ func handlePeripheralManagerDidAddService(_ peripheral: CBPeripheralManager, service: CBService, error: Error?) {
168
+ if let error = error {
169
+ print("BleAdvertiser: Failed to add GATT service: \(error.localizedDescription)")
170
+ self.broadcastPromise?.reject("GATT_SERVICE_FAILED", error.localizedDescription)
171
+ self.broadcastPromise = nil
172
+ return
173
+ }
174
+
175
+ print("BleAdvertiser: GATT service added, starting advertisement")
176
+
177
+ var serviceUUIDs: [CBUUID] = []
178
+ for uuidString in self.currentAdvertisingUUIDs {
179
+ serviceUUIDs.append(CBUUID(string: uuidString))
180
+ }
181
+
182
+ let advertisementData: [String: Any] = [CBAdvertisementDataServiceUUIDsKey: serviceUUIDs]
183
+ peripheral.startAdvertising(advertisementData)
184
+ }
185
+
186
+ func handlePeripheralManagerDidReceiveRead(_ peripheral: CBPeripheralManager, request: CBATTRequest) {
187
+ guard request.characteristic.uuid == self.broadcastIdCharacteristic?.uuid else {
188
+ peripheral.respond(to: request, withResult: .attributeNotFound)
189
+ return
190
+ }
191
+
192
+ guard let broadcastId = self.currentBroadcastId,
193
+ let data = broadcastId.data(using: .utf8) else {
194
+ peripheral.respond(to: request, withResult: .unlikelyError)
195
+ return
196
+ }
197
+
198
+ guard request.offset <= data.count else {
199
+ peripheral.respond(to: request, withResult: .invalidOffset)
200
+ return
201
+ }
202
+
203
+ request.value = data.subdata(in: request.offset..<data.count)
204
+ peripheral.respond(to: request, withResult: .success)
205
+ print("BleAdvertiser: Responded to GATT read request with broadcastId: \(broadcastId)")
206
+ }
207
+
208
+ func handlePeripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
209
+ if let error = error {
210
+ print("BleAdvertiser: Failed to start advertising: \(error.localizedDescription)")
211
+ self.isAdvertising = false
212
+ self.sendEvent("onAdvertisingFailed", [
213
+ "error": error.localizedDescription,
214
+ "uuids": self.currentAdvertisingUUIDs
215
+ ])
216
+ self.broadcastPromise?.reject("ADVERTISING_FAILED", error.localizedDescription)
217
+ } else {
218
+ print("BleAdvertiser: Successfully started advertising")
219
+ self.isAdvertising = true
220
+ self.sendEvent("onAdvertisingStarted", ["uuids": self.currentAdvertisingUUIDs])
221
+ self.broadcastPromise?.resolve("Advertising started successfully")
222
+ }
223
+ self.broadcastPromise = nil
224
+ }
225
+
226
+ // MARK: - Helper Methods
227
+
228
+ private func bluetoothStateDescription(_ state: CBManagerState) -> String {
229
+ switch state {
230
+ case .unknown:
231
+ return "unknown"
232
+ case .resetting:
233
+ return "resetting"
234
+ case .unsupported:
235
+ return "unsupported"
236
+ case .unauthorized:
237
+ return "unauthorized"
238
+ case .poweredOff:
239
+ return "powered off"
240
+ case .poweredOn:
241
+ return "powered on"
242
+ @unknown default:
243
+ return "unknown state"
244
+ }
245
+ }
246
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@blunt-ly/ble-advertiser",
3
+ "version": "0.0.0",
4
+ "description": "Minimal BLE advertiser for React-Native, optimized for background processing.",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "node internal/module_scripts/build.js",
9
+ "clean": "node internal/module_scripts/clean.js",
10
+ "lint": "eslint src/",
11
+ "prepare": "node internal/module_scripts/prepare.js",
12
+ "open:ios": "node internal/module_scripts/open-ios.js",
13
+ "open:android": "node internal/module_scripts/open-android.js"
14
+ },
15
+ "keywords": [
16
+ "react-native",
17
+ "expo",
18
+ "bluetooth",
19
+ "ble",
20
+ "advertiser",
21
+ "ble-advertiser"
22
+ ],
23
+ "repository": "https://github.com/Blunt-ly/ble-advertiser",
24
+ "bugs": {
25
+ "url": "https://github.com/Blunt-ly/ble-advertiser/issues"
26
+ },
27
+ "author": "The Blunt.ly Authors",
28
+ "license": "MIT",
29
+ "homepage": "https://github.com/Blunt-ly/ble-advertiser#readme",
30
+ "files": [
31
+ "build",
32
+ "src",
33
+ "android",
34
+ "ios",
35
+ "expo-module.config.json",
36
+ "LICENSE",
37
+ "README.md"
38
+ ],
39
+ "dependencies": {},
40
+ "devDependencies": {
41
+ "@types/react": "~19.1.1",
42
+ "eslint": "~9.39.4",
43
+ "eslint-config-universe": "^15.0.3",
44
+ "expo": "^56.0.3",
45
+ "react-native": "0.82.1",
46
+ "typescript": "^5.9.2"
47
+ },
48
+ "peerDependencies": {
49
+ "expo": "*",
50
+ "react": "*",
51
+ "react-native": "*"
52
+ }
53
+ }
@@ -0,0 +1,20 @@
1
+ export type AdvertisingStartedEvent = {
2
+ uuids: string[];
3
+ };
4
+
5
+ export type AdvertisingFailedEvent = {
6
+ error: string;
7
+ errorCode?: number;
8
+ uuids?: string[];
9
+ };
10
+
11
+ export type AdvertisingStoppedEvent = {
12
+ uuids?: string[];
13
+ stoppedCount?: number;
14
+ };
15
+
16
+ export type BleAdvertiserModuleEvents = {
17
+ onAdvertisingStarted: (params: AdvertisingStartedEvent) => void;
18
+ onAdvertisingFailed: (params: AdvertisingFailedEvent) => void;
19
+ onAdvertisingStopped: (params: AdvertisingStoppedEvent) => void;
20
+ };
@@ -0,0 +1,33 @@
1
+ import { NativeModule, requireNativeModule } from 'expo';
2
+
3
+ import { BleAdvertiserModuleEvents } from './BleAdvertiser.types';
4
+
5
+ declare class BleAdvertiserModule extends NativeModule<BleAdvertiserModuleEvents> {
6
+ /**
7
+ * Start BLE advertising with the specified service UUIDs
8
+ * @param uuids Array of service UUID strings to advertise
9
+ * @returns Promise that resolves when advertising starts successfully
10
+ */
11
+ broadcast(uuids: string[]): Promise<string>;
12
+
13
+ /**
14
+ * Stop all BLE advertising
15
+ * @returns Promise that resolves with information about stopped advertisers
16
+ */
17
+ stopBroadcast(): Promise<{ stopped: boolean; stoppedAdvertisers?: number }>;
18
+
19
+ /**
20
+ * Check if BLE advertising is supported on this device
21
+ * @returns Promise that resolves to true if supported, false otherwise
22
+ */
23
+ isSupported(): Promise<boolean>;
24
+
25
+ /**
26
+ * Check if Bluetooth is currently enabled
27
+ * @returns Promise that resolves to true if enabled, false otherwise
28
+ */
29
+ isEnabled(): Promise<boolean>;
30
+ }
31
+
32
+ // This call loads the native module object from the JSI.
33
+ export default requireNativeModule<BleAdvertiserModule>('BleAdvertiser');
package/src/index.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { EventSubscription } from 'expo-modules-core';
2
+ import { Platform } from 'react-native';
3
+
4
+ import {
5
+ AdvertisingStartedEvent,
6
+ AdvertisingFailedEvent,
7
+ AdvertisingStoppedEvent,
8
+ } from './BleAdvertiser.types';
9
+ import BleAdvertiserModule from './BleAdvertiserModule';
10
+ import { expand16BitUuid } from './utils';
11
+
12
+ // HACK: Android's BluetoothLeAdvertiser only accepts 128-bit service UUIDs, while
13
+ // CoreBluetooth on iOS accepts the 16-bit short form directly. Expand any
14
+ // 16-bit input before handing it to the Android native module.
15
+ function normalizeUuidsForPlatform(uuids: string[]): string[] {
16
+ if (Platform.OS !== 'android') return uuids;
17
+ return uuids.map((uuid) => {
18
+ const hex = uuid.replace(/[^0-9a-fA-F]/g, '');
19
+ return hex.length === 4 ? expand16BitUuid(uuid) : uuid;
20
+ });
21
+ }
22
+
23
+ /**
24
+ * Start BLE advertising with the specified service UUIDs
25
+ * @param uuids Array of service UUID strings to advertise
26
+ * @returns Promise that resolves when advertising starts successfully
27
+ */
28
+ export function broadcast(uuids: string[]): Promise<string> {
29
+ return BleAdvertiserModule.broadcast(normalizeUuidsForPlatform(uuids));
30
+ }
31
+
32
+ /**
33
+ * Stop all BLE advertising
34
+ * @returns Promise that resolves with information about stopped advertisers
35
+ */
36
+ export function stopBroadcast(): Promise<{ stopped: boolean; stoppedAdvertisers?: number }> {
37
+ return BleAdvertiserModule.stopBroadcast();
38
+ }
39
+
40
+ /**
41
+ * Check if BLE advertising is supported on this device
42
+ * @returns Promise that resolves to true if supported, false otherwise
43
+ */
44
+ export function isSupported(): Promise<boolean> {
45
+ return BleAdvertiserModule.isSupported();
46
+ }
47
+
48
+ /**
49
+ * Check if Bluetooth is currently enabled
50
+ * @returns Promise that resolves to true if enabled, false otherwise
51
+ */
52
+ export function isEnabled(): Promise<boolean> {
53
+ return BleAdvertiserModule.isEnabled();
54
+ }
55
+
56
+ /**
57
+ * Add listener for advertising started events
58
+ * @param listener Function to call when advertising starts
59
+ * @returns EventSubscription that can be used to remove the listener
60
+ */
61
+ export function addAdvertisingStartedListener(
62
+ listener: (event: AdvertisingStartedEvent) => void
63
+ ): EventSubscription {
64
+ return BleAdvertiserModule.addListener('onAdvertisingStarted', listener);
65
+ }
66
+
67
+ /**
68
+ * Add listener for advertising failed events
69
+ * @param listener Function to call when advertising fails
70
+ * @returns EventSubscription that can be used to remove the listener
71
+ */
72
+ export function addAdvertisingFailedListener(
73
+ listener: (event: AdvertisingFailedEvent) => void
74
+ ): EventSubscription {
75
+ return BleAdvertiserModule.addListener('onAdvertisingFailed', listener);
76
+ }
77
+
78
+ /**
79
+ * Add listener for advertising stopped events
80
+ * @param listener Function to call when advertising stops
81
+ * @returns EventSubscription that can be used to remove the listener
82
+ */
83
+ export function addAdvertisingStoppedListener(
84
+ listener: (event: AdvertisingStoppedEvent) => void
85
+ ): EventSubscription {
86
+ return BleAdvertiserModule.addListener('onAdvertisingStopped', listener);
87
+ }
88
+
89
+ // Export the types for consumers
90
+ export * from './BleAdvertiser.types';
package/src/utils.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Expands a 16-bit UUID to the full 128-bit Bluetooth UUID format.
3
+ *
4
+ * @param shortUuid 16-bit UUID string (with or without dashes)
5
+ * @returns Expanded 128-bit UUID string
6
+ * @throws Error if the input is not a valid 16-bit UUID
7
+ */
8
+ export function expand16BitUuid(shortUuid: string): string {
9
+ // Clean and validate input
10
+ const cleaned = shortUuid.replace(/[^0-9a-fA-F]/g, '');
11
+
12
+ if (!/^[0-9a-fA-F]{4}$/i.test(cleaned)) {
13
+ throw new Error(`Invalid 16-bit UUID: ${shortUuid}`);
14
+ }
15
+
16
+ // Pad to 4 chars and convert to lowercase
17
+ const paddedUuid = cleaned.padStart(4, '0').toLowerCase();
18
+
19
+ // Bluetooth Base UUID: 00000000-0000-1000-8000-00805F9B34FB
20
+ return `0000${paddedUuid}-0000-1000-8000-00805f9b34fb`;
21
+ }