Skip to main content

Step 6: Initialise Ovok, authentication, and Bluetooth

What we are building​

One persistent OvokClient, CMS-backed i18n at the root, email/password Patient registration and sign-in, protected patient routes, and one foreground BTProvider mounted only after authentication.

What you should already have​

  • Sandbox ovokConfig, including tenant and reviewed Questionnaire identifiers.
  • The app bootstrap and development build from step 1.
  • The stable acceptedDevices array from step 4.
  • CmsTranslations and localizationReady from step 3.

The implementation​

Create one Ovok client​

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",
storage: new ExpoClientStorage(),
});

Keep the client at module scope. Its storage adapter preserves the client session across app restarts. See client configuration.

Compose the root providers​

Update app/_layout.tsx:

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

import { CmsTranslations } from "../src/localization/cms-translations";
import { localizationReady } from "../src/localization/i18n";
import { ovokClient } from "../src/client/ovok-client";

function LocalizationRoot() {
const [ready, setReady] = React.useState(false);

React.useEffect(() => {
void localizationReady.then(() => setReady(true));
}, []);

if (!ready) return <ActivityIndicator />;
return (
<>
<CmsTranslations />
<Stack />
</>
);
}

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,
}}
>
<LocalizationRoot />
</ThemeProvider>
</OvokProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
</KeyboardProvider>
);
}

If CMS text is not ready, screens can use a short native bootstrap fallback and retry the CMS request. Do not make authentication depend on the availability of translations.

Add the Patient sign-in and registration routes​

Create app/sign-in.tsx:

import { SignIn, useSession } from "@ovok/native";
import { Redirect, useRouter } from "expo-router";
import { ActivityIndicator, View } from "react-native";

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

export default function SignInScreen() {
const router = useRouter();
const { status } = useSession({ sessions: false });

if (status === "loading") return <ActivityIndicator />;
if (status === "authenticated") return <Redirect href="/(patient)" />;

return (
<View>
<SignIn>
<SignIn.Header>
<SignIn.Header.Title />
<SignIn.Header.Description />
</SignIn.Header>
<SignIn.EmailForm
loginType="Patient"
tenantCode={ovokConfig.tenantCode}
onSuccess={() => router.replace("/(patient)")}
>
<SignIn.EmailForm.Inputs />
<SignIn.EmailForm.SigninButton />
</SignIn.EmailForm>
<SignIn.RegisterLink onPress={() => router.push("/register")} />
</SignIn>
</View>
);
}

Create app/register.tsx using the native Patient registration component:

import { Register, useSession } from "@ovok/native";
import { Redirect, useRouter } from "expo-router";
import { ActivityIndicator, View } from "react-native";

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

export default function RegisterScreen() {
const router = useRouter();
const { status } = useSession({ sessions: false });

if (status === "loading") return <ActivityIndicator />;
if (status === "authenticated") return <Redirect href="/(patient)" />;

return (
<View>
<Register>
<Register.Header>
<Register.Header.Title />
<Register.Header.Description />
</Register.Header>
<Register.EmailForm
tenantCode={ovokConfig.tenantCode}
onSuccess={() => router.replace("/(patient)")}
>
<Register.EmailForm.Inputs />
<Register.EmailForm.RegisterButton />
</Register.EmailForm>
<Register.LoginLink onPress={() => router.push("/sign-in")} />
</Register>
</View>
);
}

These components manage the supported account workflow. Registration creates the Patient; do not make a second Patient resource. Read the Native authentication guide and Patient registration setup.

Protect the Patient area and mount BLE there​

Create app/index.tsx to choose the first route:

import { useSession } from "@ovok/native";
import { Redirect } from "expo-router";
import { ActivityIndicator } from "react-native";

export default function IndexScreen() {
const { status } = useSession({ sessions: false });
if (status === "loading") return <ActivityIndicator />;
return status === "authenticated" ? (
<Redirect href="/(patient)" />
) : (
<Redirect href="/sign-in" />
);
}

Create app/(patient)/_layout.tsx:

import { useSession } from "@ovok/native";
import { Redirect, Tabs } from "expo-router";
import { ActivityIndicator } from "react-native";

import { PatientBluetoothWorkspace } from "../../src/features/bluetooth/patient-bluetooth";

export default function PatientLayout() {
const { status } = useSession({ sessions: false });
if (status === "loading") return <ActivityIndicator />;
if (status !== "authenticated") return <Redirect href="/sign-in" />;

return (
<PatientBluetoothWorkspace>
<Tabs>
<Tabs.Screen name="index" options={{ title: "Screening" }} />
<Tabs.Screen name="devices" options={{ title: "Devices" }} />
<Tabs.Screen name="history" options={{ title: "History" }} />
<Tabs.Screen name="settings" options={{ title: "Settings" }} />
<Tabs.Screen name="assessment" options={{ href: null }} />
</Tabs>
</PatientBluetoothWorkspace>
);
}

Create src/features/bluetooth/patient-bluetooth.tsx. This module owns the app’s single BLE manager; step 8 updates this same component to save received measurements.

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

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

const bleManager = new BleManager();

export function PatientBluetoothWorkspace({ children }: PropsWithChildren) {
return (
<BTProvider bleManager={bleManager} acceptedDevices={acceptedDevices}>
{children}
</BTProvider>
);
}

BTProvider starts discovery while the authenticated Patient tabs are mounted. It owns the BLE runtime and permission checks. Keep BleManager and acceptedDevices stable. See BTProvider and Bluetooth.

Important Ovok decisions​

  • useSession selects the signed-in route; the Patient AccessPolicy authorizes FHIR access.
  • Bluetooth is not mounted on sign-in or registration screens.
  • This app owns one BleManager and one BTProvider for the patient area.
  • onResult is added in step 8, where the app can associate a normalized result with the active Patient and save it.

Expected result​

Patient registration and sign-in are available, protected routes redirect correctly, and signing in mounts one Bluetooth runtime that can discover the allowlisted meter.

Common errors and troubleshooting​

  • Auth form returns a project error: check tenant code, Patient login/registration settings, and default Patient AccessPolicy.
  • BLE permission appears on the sign-in screen: move BTProvider into the authenticated route group.
  • Repeated scanning or unstable sessions: ensure the app does not mount a second BLE provider or recreate the module-level BleManager.
  • A signed-out user can open a Patient screen: keep the route guard and verify the FHIR AccessPolicy independently.

Previous / Next​

Previous: add the FINDRISC Questionnaire · Next: build the screening home