GuidesComponentsBluetooth management

BTProvider

Own a Bluetooth LE session and receive device, connection, measurement, and error events.

  • Stable unknown
  • Release candidate since 1.16.1-rc.3
  • Development since 1.16.1-dev.2111+9796905

BTProvider is the React boundary for an SDK-managed Bluetooth session. It requests runtime permissions, starts discovery for the device declarations you accept, exposes the runtime to Bluetooth hooks and UI, and releases managed devices when the provider unmounts.

Import it from @ovok/native/bt-management.

Minimal setup

import { BleManager } from "react-native-ble-plx";
import { BTProvider, IntegratedDevices } from "@ovok/native/bt-management";

const bleManager = new BleManager();
const acceptedDevices = [IntegratedDevices.BP2] as const;

<BTProvider
  bleManager={bleManager}
  acceptedDevices={acceptedDevices}
  onDeviceFound={(device) => device.connect()}
  onResult={(result) => saveMeasurement(result)}
>
  <MeasurementScreen />
</BTProvider>;

Keep bleManager and acceptedDevices stable. A new array on each render changes the provider's manager configuration and restarts the session.

Props

Every prop, with its type and description, is listed in the @ovok/native/bt-management API reference.

Background result delivery

backgroundSync is opt-in. It enables background scan behavior, creates a durable result queue, and starts the provider-managed Android connected-device foreground service. The queue configuration requires persistent storage and an asynchronous delivery callback:

<BTProvider
  bleManager={bleManager}
  acceptedDevices={acceptedDevices}
  backgroundSync={{
    enabled: true,
    storage: {
      getItem: (key) => measurementStore.getItem(key),
      setItem: (key, value) => measurementStore.setItem(key, value),
    },
    getOwnerKey: () => authStore.currentAccountKey,
    onResult: async (result) => {
      await uploadMeasurement(result, {
        accountKey: result.ownerKey,
        idempotencyKey: result.id,
      });
    },
    android: {
      foregroundService: {
        notificationTitle: "Bluetooth monitoring",
        notificationBody: "Listening for health measurements",
      },
    },
  }}
>
  <MeasurementScreen />
</BTProvider>

storage must survive process restarts. The queue writes the result before invoking onResult and removes it after that promise resolves. Delivery is at least once, so deduplicate server writes by result.id. The result also includes queueId, queuedAt, and attempt. Failed items remain queued for automatic retry; default retry settings are three attempts with a 30-second exponential backoff base. The coordinator checks the active getOwnerKey() before each delivery and each result includes the ownerKey captured when it was queued. Use that value to select the account for persistence. A callback already running cannot be canceled by an account switch; its remaining queued results wait until their owner is active again.

The provider's separate onResult prop remains an immediate measurement callback. It is not the queue acknowledgement and its return value is not awaited. Use backgroundSync.onResult for app-owned delivery that must complete before an item is removed from the queue.

backgroundSync.android.foregroundService controls the provider-managed Android service notification and optional battery-optimization request. If the app owns the BLE scan outside this provider, use the standalone AndroidBluetoothForegroundService instead. Do not start both service owners for the same scan.

For the iOS restoration identifier, service UUID filters, Android native setup, and HealthKit or Health Connect background paths, see the background sync guide.

Permission behavior

By default, the provider requests Bluetooth permissions on mount. Android 12 and later requests BLUETOOTH_CONNECT and BLUETOOTH_SCAN; earlier Android versions request fine location. iOS requests Bluetooth access. It checks again when the app returns from Settings after the app opens Settings through the permission flow. Configure the corresponding native declarations in the installation guide.

When gateOnPermission is true, the provider renders a loading indicator while the automatic request is unresolved. If access is denied, it calls permissionFallback when supplied; otherwise it renders DefaultPermissionFallback, which opens the app settings.

To ask only after the patient chooses to pair, set requestPermissionOnMount={false}. BTProvider and its runtime stay mounted, but discovery waits until a child calls useBluetoothState().requestPermission() and the request succeeds. The hook must render beneath BTProvider; its returned promise resolves to whether access was granted. Handle a rejected promise in the app. In this mode, gateOnPermission does not gate children, so keep the pairing action available and use the hook's permissionStatus to gate other UI if needed.

import { Button } from "react-native";
import { useBluetoothState } from "@ovok/native/bt-management";

function PairButton() {
  const { requestPermission } = useBluetoothState();

  return (
    <Button
      title="Pair a device"
      onPress={() => {
        void requestPermission().catch((error) =>
          console.warn("Bluetooth permission request failed", error),
        );
      }}
    />
  );
}

<BTProvider
  bleManager={bleManager}
  acceptedDevices={acceptedDevices}
  requestPermissionOnMount={false}
>
  <PairButton />
  <MeasurementScreen />
</BTProvider>;

Runtime lifetime

The provider creates its manager only when acceptedDevices has at least one entry and starts scanning after callback subscriptions are installed. With requestPermissionOnMount={false}, scanning instead waits until the app-triggered request succeeds. On teardown it stops the scan, removes listeners, and destroys the manager. Place the provider at the lowest stable tree level that should keep the scan active.

Bluetooth hooks and PairingFlow must render beneath this provider. A route may read runtime data while the provider is mounted, but it should not create a competing manager of its own.

Last updated on

On this page