Skip to main content

Step 5: Initialise Ovok and Bluetooth

What we are building​

One persistent OvokClient for the sandbox project and one foreground Bluetooth runtime that discovers only the two devices selected in step 4.

What you should already have​

  • src/config/ovok.ts from step 2.
  • acceptedDevices from step 4.
  • The native development build from step 1.

The implementation​

Create the client once​

Create src/client/ovok-client.ts:

import { OvokClient } from "@ovok/core";
import { ExpoClientStorage } from "@ovok/native/polyfills";

import { ovokConfig } from "../config/ovok";

export const ovokClient = new OvokClient({
baseUrl: ovokConfig.baseUrl,
fhirUrlPath: "/fhir/R4/",
storage: new ExpoClientStorage(),
});

The client stays at module scope so screens share the same session and storage. The FHIR path includes the trailing slash. See Core client configuration.

Compose the app providers​

Update app/_layout.tsx:

import { OvokProvider } from "@ovok/core";
import {
DEFAULT_COLORS,
DEFAULT_MULTIPLIERS,
ThemeProvider,
} from "@ovok/native/theme";
import { Stack } from "expo-router";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";

import { LocalizationGate } from "../src/localization/localization-gate";
import { ovokClient } from "../src/client/ovok-client";

export default function RootLayout() {
return (
<KeyboardProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<OvokProvider client={ovokClient}>
<ThemeProvider
theme={{
colors: DEFAULT_COLORS,
dark: false,
spacingMultiplier: DEFAULT_MULTIPLIERS.spacing,
borderRadiusMultiplier: DEFAULT_MULTIPLIERS.borderRadius,
}}
>
<LocalizationGate>
<Stack />
</LocalizationGate>
</ThemeProvider>
</OvokProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
</KeyboardProvider>
);
}

The native auth and themed UI components render below both OvokProvider and the SDK ThemeProvider. The keyboard and gesture providers support the native form components.

Mount Bluetooth in the signed-in patient area​

Create src/features/bluetooth/device-state.tsx:

import { BTProvider } from "@ovok/native/bt-management";
import { BleManager } from "react-native-ble-plx";
import * as React from "react";

import { acceptedDevices } from "./accepted-devices";

type DeviceState = {
devices: Record<
string,
{ name: string; status: string; batteryLevel?: number; hasError?: boolean }
>;
};

export const DeviceStateContext = React.createContext<DeviceState | null>(null);

const bleManager = new BleManager();

export function BluetoothWorkspace({
children,
}: React.PropsWithChildren) {
const [devices, setDevices] = React.useState<DeviceState["devices"]>({});

return (
<DeviceStateContext.Provider value={{ devices }}>
<BTProvider
bleManager={bleManager}
acceptedDevices={acceptedDevices}
onDeviceStatusChanged={({ deviceData, status }) => {
setDevices((current) => ({
...current,
[deviceData.id]: {
...current[deviceData.id],
name: deviceData.model || deviceData.localName || "",
status,
},
}));
}}
onConnectionEvent={({ deviceData, batteryLevel }) => {
if (typeof batteryLevel === "number") {
setDevices((current) => ({
...current,
[deviceData.id]: {
...current[deviceData.id],
name: deviceData.model || deviceData.localName || "",
status: current[deviceData.id]?.status ?? "Disconnected",
batteryLevel,
},
}));
}
}}
onError={({ deviceData }) => {
if (deviceData) {
setDevices((current) => ({
...current,
[deviceData.id]: {
...current[deviceData.id],
name: deviceData.model || deviceData.localName || "",
status: current[deviceData.id]?.status ?? "Disconnected",
hasError: true,
},
}));
}
}}
>
{children}
</BTProvider>
</DeviceStateContext.Provider>
);
}

Add app/(patient)/_layout.tsx:

import { Slot } from "expo-router";

import { BluetoothWorkspace } from "../../src/features/bluetooth/device-state";

export default function PatientLayout() {
return (
<BluetoothWorkspace>
<Slot />
</BluetoothWorkspace>
);
}

Keep patient routes inside app/(patient). The workspace exists only after authentication, so Bluetooth permission is not requested on the sign-in screen. The app can read state with React.useContext(DeviceStateContext) from a screen beneath this layout.

Important Ovok decisions​

  • BTProvider owns discovery and connection lifecycle while mounted. Do not create a new BleManager during a component render.
  • The provider reports normalized result data; it does not save readings or choose the Patient. Step 7 adds the explicit save operation after the authenticated Patient is known.
  • Device IDs may be local Bluetooth identifiers. Keep them as internal map keys and do not display or log them.
  • Battery level is optional and only reported when the device exposes it.

Expected result​

After a patient enters the patient route group, the app requests Bluetooth permission and the runtime can discover compatible BP2 and F4 devices. The permission state and connection errors are available to child screens.

Common errors and troubleshooting​

  • Permission dialog never appears: check the iOS Bluetooth plugin entries or Android manifest generated by Prebuild, then rebuild.
  • No devices appear in a simulator: use a physical phone; a simulator cannot emulate the actual BLE devices.
  • Repeated scans or connection instability: confirm there is only one mounted BluetoothWorkspace and one BleManager.
  • Permission is granted but scan fails: check that Bluetooth is switched on and that the app was rebuilt after native config changes.

Next step​

Continue to step 6: build onboarding and authentication.