Reusable SDK features
The SDK exposes integration primitives and optional composition UI, not an application
workflow. A host can enable one supported device, register a custom device, or use the
complete catalog. Patient selection, authentication, server upload, storage,
notifications, and navigation remain host decisions; the optional ui module provides
themeable flows that accept the host's theme and primitive slots.
Bluetooth hooks
Render these hooks below BTProvider:
const { state, requestPermission } = useBluetoothState({ bleManager });
const { devices } = useNearbyDevices();
const { paired, pair, forget } = usePairedDevices({ storage });
const connection = useDeviceConnection(devices[0]?.id);
const { measurements } = useMeasurements(onResult, {
policy: "first-reading-per-kind-per-connection",
});
useBluetoothState reports on, off, unauthorized, or unsupported. The device,
connection, and measurement hooks expose SDK events without prescribing how a host
renders or uploads them. usePairedDevices requires an app-supplied storage adapter;
the SDK does not assume an account, patient, or storage key.
resultPolicies can be supplied to BTProvider per device. Available policies are
all, daily-totals-only, and first-reading-per-kind-per-connection; each device
can also override the policy by MeasurementTypeKey through byMeasurement.
Connection events include pairing, bonded, history-download progress, and standard
battery level. History-download progress is emitted once per accepted history reading;
progress.total is currently omitted, so consumers should treat progress.current
as a delivered-reading count rather than a percentage denominator.
Every delivered result has a stable, non-enumerable id: JSON serialization, object
spread, and Object.keys do not include it, so read it directly before copying the
payload. A device history record sent again receives
the same identity when its device identity and measurement payload are unchanged.
Background sync and storage
useBackgroundSync exposes enabled, setEnabled, and the states on, off,
restricted, and blocked. It toggles the manager's scan policy; it does not itself
run the result queue or schedule uploads. Supply canEnable and onEnabledChange
when the host has additional platform policy. BTProvider.backgroundSync and its
BackgroundSyncCoordinator persist before delivery and retry failed deliveries; the
queue passes queuedAt, attempt, queueId, and the stable result id to the host
callback.
For credentials needed by a background relaunch, use
createBackgroundSafeClientStorage with an injected SecureStore-compatible adapter.
The exported BackgroundSafeSecureStore, BackgroundSafeStorageOptions, and
BackgroundSafeClientStorage types describe that boundary. Reads that fail before
first unlock never delete the stored value; call reload after unlock and retry.
The adapter does not own authentication or choose a host key. Test the supplied
adapter on each target OS: keychainAccessible is the numeric
SecureStore.AFTER_FIRST_UNLOCK value (the default is 0), and secure-store writes
can still fail before native keychain setup or first unlock. On iOS, the current
expo-secure-store integration can reject the adapter's AFTER_FIRST_UNLOCK write
options, so writes may throw instead of becoming background-safe; verify that the
installed native version accepts the adapter's option shape before relying on writes
for a background relaunch. Surface write failures and provide a host-owned retry path.
The public boundary is intentionally small and app-owned:
interface BackgroundSafeSecureStore {
getItemAsync(key: string, options?: Record<string, unknown>): Promise<string | null>;
setItemAsync(key: string, value: string, options?: Record<string, unknown>): Promise<void>;
deleteItemAsync?(key: string, options?: Record<string, unknown>): Promise<void>;
}
interface BackgroundSafeStorageOptions {
secureStore: BackgroundSafeSecureStore;
keyListKey?: string;
keychainService?: string;
keychainAccessible?: number;
}
interface BackgroundSafeClientStorage {
getItem(key: string): Promise<string | null>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
clear(): Promise<void>;
reload(): Promise<void>;
}
Health import
useHealthImport accepts a host-provided adapter for status and authorization. Its
getStatus(type) result is one of authorized, shouldRequest, refused, or
unsupported; request(types, background) receives the complete requested type list
and the hook-level background flag. The generic hook has no per-type backgroundAccess
field. Platform-specific Android background permission status belongs to
DataSync.AndroidHealthConnectAuthorizationProvider.
Expo native setup
The optional @ovok/native config plugin only adds capabilities explicitly enabled by
the host:
module.exports = [
["@ovok/native", {
bluetooth: { background: true },
healthKit: true,
healthConnect: true,
backgroundSync: true,
notifications: false,
}],
];
The plugin does not add the expo-notifications plugin unless notifications: true.
bluetooth.background enables iOS background-central configuration, while
backgroundSync enables the Android background service path. With healthConnect: true, a truthy backgroundSync value also enables scheduled Health Connect work.
Inspect the merged native project. Native modules still require a development build;
Expo Go cannot load them.
Test kit
createFakeBleManager and replayRecordedFrames allow scan, parser, policy, and hook
tests to run without a peripheral. Apps can keep their own recorded fixture registry
with defineRecordedDeviceFrames and replay only the devices they support.
Error contract
Native failures can be normalized with toBluetoothError. The stable
BluetoothErrorCode is separate from the original error and optional translation key.
Existing measurement translation-key errors remain compatible with earlier callbacks.
Release policy
Behavior changes, new hooks, result identity changes, queue semantics, and native setup
changes are minor-release work. Patch releases are reserved for fixes that do not
change runtime behavior. A release that raises the required @ovok/core range must
release core first, then update the native peer range and package version.