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
acceptedDevicesarray from step 4. CmsTranslationsandlocalizationReadyfrom 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
useSessionselects the signed-in route; the Patient AccessPolicy authorizes FHIR access.- Bluetooth is not mounted on sign-in or registration screens.
- This app owns one
BleManagerand oneBTProviderfor the patient area. onResultis 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
BTProviderinto 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