@su-engineering/react-native-nearby-interaction
v0.1.0
Published
iOS UWB accessory ranging for React Native, with a T-TAG BLE profile and external transports
Readme
React Native Nearby Interaction
iPhone → UWB accessory ranging through Apple's Nearby Interaction framework. This standalone MVP has a configurable Truesense T-TAG BLE profile, an external transport API for other accessories, a typed event API, and a React hook.
Current release: 0.1.0, published under the latest npm tag. Initial physical distance ranging, stop/restart, foreground resume and
tag reconnection passed on an iPhone 16 Pro with a Truesense T-TAG. See
validation for the evidence and remaining acceptance tests.
Scope
| Capability | MVP | | --- | --- | | iOS UWB accessory sessions | Implemented | | Truesense demo firmware handshake | Device-tested with a T-TAG; firmware revision not recorded | | Configurable BLE UUIDs and framing bytes | Implemented | | Other accessory transports | External configuration and shareable-data API | | Nullable distance, horizontal angle, direction vector | Implemented | | Bounded retries, deadlines, write acknowledgements, stale reading cleanup | Implemented | | React Native New Architecture | TurboModule + generated event bindings | | Expo | Info.plist config plugin; native development build required | | Android, iPhone-to-iPhone, multiple simultaneous tags | Outside this MVP | | Background ranging | Outside this MVP; disconnects and resumes in foreground |
Compatibility target: React Native 0.83.x (baseline 0.83.1), React 19.2.x, Expo SDK 55 (baseline 55.0.0), iOS 15.1+, Xcode 26.2+ for Expo builds. Other versions are not claimed by this release. Hardware support is checked at runtime; simulators cannot provide physical UWB measurements.
Installation
Install from npm:
npm install @su-engineering/react-native-nearby-interactionBefore release, install the tarball produced by npm run pack:check, or clone
the repository and install locally:
npm install /absolute/path/to/react-native-nearby-interaction
cd ios
bundle exec pod installAdd nonempty NSNearbyInteractionUsageDescription and
NSBluetoothAlwaysUsageDescription strings to your app's Info.plist. The library
checks their presence before starting a built-in BLE session.
For Expo, add the config plugin and rebuild your native app:
{
"expo": {
"plugins": [["@su-engineering/react-native-nearby-interaction", {
"nearbyInteractionUsageDescription": "Measure your distance to your UWB tag.",
"bluetoothUsageDescription": "Connect to your UWB tag."
}]]
}
}The plugin preserves existing descriptions unless explicitly overridden and does not add background modes. Expo Go cannot load this native module.
Basic usage
import {Text} from 'react-native';
import {nearbyInteraction, useNearbyInteraction} from '@su-engineering/react-native-nearby-interaction';
function Distance() {
const {state, measurement, error} = useNearbyInteraction();
return <Text>{error?.message ?? `${state}: ${measurement?.distance ?? '—'} m`}</Text>;
}
const capabilities = await nearbyInteraction.getCapabilities();
if (capabilities.supported) {
await nearbyInteraction.start({namePrefix: 'T-TAG', maxRetries: 3});
}
await nearbyInteraction.stop();Omit the name filter if your firmware advertises another name. By default, the
first matching peripheral is selected. Use deviceId (CoreBluetooth UUID) to
target a particular unit. The default scan timeout is 15 seconds.
start() resolves when a run is accepted, not when the first distance arrives.
Subscribe before starting and observe ranging/measurement for progress.
Use one shared session; mounting/unmounting the hook only subscribes. Call stop()
when your application no longer owns the session. Call stop() before replacing
an active or failed run. Calls are serialized, including stop queued behind start.
Measurements are never filtered by a proximity threshold. Delivery defaults to 100 ms, and the snapshot clears its measurement after 2 seconds without an update. Movement beyond your application's threshold remains visible. Both distance and direction may independently be unavailable.
Configure the accessory profile
import {nearbyInteraction, TTAG_PROFILE} from '@su-engineering/react-native-nearby-interaction';
await nearbyInteraction.start({
transport: 'ttag',
profile: {
...TTAG_PROFILE,
serviceUUID: '6E400001-B5A3-F393-E0A9-E50E24DCCA9E',
writeCharacteristicUUID: '6E400002-B5A3-F393-E0A9-E50E24DCCA9E',
notifyCharacteristicUUID: '6E400003-B5A3-F393-E0A9-E50E24DCCA9E',
commands: {
initialize: 0x0a, start: 0x0b, stop: 0x0c,
configuration: 0x01, started: 0x02, stopped: 0x03,
},
allowChunkedWrites: false,
},
});A custom profile must supply the three primary UUIDs. Optional
configurationServiceUUID and configurationCharacteristicUUID are provided
together for a raw configuration notification; these may share the primary
service. Custom profiles do not implicitly inherit the Truesense fallback
characteristic. UUIDs must use their full 128-bit form.
The built-in transport requires write-with-response and notify/indicate support. It waits for service discovery and notification subscriptions before requesting configuration. Its command format is one-byte opcodes followed by opaque Apple configuration bytes. It does not assume a 37-byte configuration or attempt to deserialize an accessory as an iPhone peer.
Oversized outgoing messages are rejected before writing. Opt into MTU chunking only when your firmware reassembles a command across acknowledged BLE writes. Incoming notifications must contain a complete frame; firmware-specific fragmentation/reassembly belongs in an external transport.
Other accessory protocols
Use transport: 'external' to own discovery, connection, framing, and negotiation.
The native module continues to own the Apple NI session:
const configSubscription = nearbyInteraction.on('configuration', ({data}) => {
// Raw Apple shareable configuration, base64. Add your firmware's framing and
// send over BLE/Wi-Fi/etc. Handle transport errors in your application.
});
await nearbyInteraction.start({transport: 'external'});
// After receiving your accessory's Apple NI configuration:
await nearbyInteraction.configureAccessory(accessoryConfigBase64);The peer UUID is optional. Supply it only for an accessory that is Bluetooth
paired, actively connected, and implements Apple's standard Nearby Interaction
GATT service and Accessory Configuration Characteristic. A CoreBluetooth
connection alone does not meet these requirements; omit the UUID for ordinary
foreground ranging over a vendor transport. The built-in T-TAG transport uses
the foreground configuration initializer. On retry or
foreground resume, a transition to configuring requests fresh negotiation.
Your transport must reset/reinitialize the accessory and supply its configuration.
Register that state listener before start(). Repeated configuration for an
already configured session is ignored. Remove subscriptions and close your own
transport after stop(); it only tears down the native NI session in external mode.
No default framing, token discovery, credential transfer, or generic UWB radio scanning is implied by external mode. The accessory must implement an Apple-compatible NI protocol.
Events and errors
const subscription = nearbyInteraction.on('measurement', reading => {
console.log(reading.distance, reading.horizontalAngle);
});
subscription.remove();Events: state, connected, disconnected, measurement, configuration,
data, error. Application data is opaque base64; the library never opens URLs
or performs wallet actions. sendData(base64) resolves after all BLE write
acknowledgements and is available only in built-in BLE mode.
Recoverable connection/session failures use exponential delays of 0.5–4 seconds,
with a total retry budget per run. Permission denial, invalid Apple configuration,
and incompatible GATT profiles fail immediately. Terminal failure emits an error
and failed; stop and start after fixing the cause. The error snapshot retains
the last error until a connection/measurement succeeds or the run is reset.
Development and example
npm ci
npm run check
npm ci --prefix example
cd example
bundle install
bundle exec pod install --project-directory=ios
npm startOpen example/ios/NearbyInteractionExample.xcworkspace, select your signing team
and a unique bundle identifier, and run on your UWB-capable iPhone. Alternatively
run npm run ios -- --device "Your iPhone" in another terminal after signing is
configured. The example displays capability, state, distance, angle, direction,
and a bounded event log. Swift protocol tests run with swift test on macOS.
See architecture, hardware validation,
and source lineage. This package is MIT licensed; the example
retains the official template's MIT license in example/TEMPLATE-LICENSE.
See contributing, security and the
release guide.
