Step 6: Build onboarding and authentication
What we are building
An email-and-password patient flow with registration, sign-in, and a protected patient route group. The native forms call the configured Ovok client and activate the returned login; the app decides what to show before and after that flow.
What you should already have
- The configured sandbox client and tenant code from step 2.
- The providers from step 5.
@ovok/nativeauthentication forms available belowOvokProviderand the nativeThemeProvider.
The implementation
Add the sign-in screen
Create app/sign-in.tsx:
import { SignIn, useSession } from "@ovok/native";
import { Redirect, useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, Alert, View } from "react-native";
import { ovokConfig } from "../src/config/ovok";
export default function SignInScreen() {
const router = useRouter();
const { t } = useTranslation();
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)")}
onError={() =>
Alert.alert(t("auth.signInFailed"), t("errors.tryAgain"))
}
>
<SignIn.EmailForm.Inputs />
<SignIn.EmailForm.SigninButton />
</SignIn.EmailForm>
<SignIn.RegisterLink onPress={() => router.push("/register")} />
</SignIn>
</View>
);
}
SignIn.EmailForm owns the standard credential and supported MFA exchange. Its onSuccess callback runs after the form activates the login. Do not store a second copy of the credentials or build a parallel authentication request. See Native authentication and session management.
Add registration
Create app/register.tsx:
import { Register, useSession } from "@ovok/native";
import { Redirect, useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, Alert, View } from "react-native";
import { Text } from "react-native-paper";
import { ovokConfig } from "../src/config/ovok";
export default function RegisterScreen() {
const router = useRouter();
const { t } = useTranslation();
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)")}
onError={() =>
Alert.alert(t("auth.registrationFailed"), t("errors.tryAgain"))
}
onNextStep={(step) => {
if (step.nextStep === "clinician-invite") {
Alert.alert(
t("auth.checkEmailTitle"),
t("auth.checkEmailForInvitation"),
);
}
}}
>
<Register.EmailForm.Inputs />
<Register.EmailForm.RegisterButton />
</Register.EmailForm>
<Register.LoginLink onPress={() => router.push("/sign-in")} />
</Register>
</View>
);
}
The optional onNextStep handles the configured business-email clinician-invite outcome. If that project setting is not used, remove the callback. The ordinary patient-registration path creates and activates the patient account through the SDK; do not create a duplicate Patient resource.
Protect the patient route group
Update app/(patient)/_layout.tsx so Bluetooth starts only after a patient is authenticated:
import { useSession } from "@ovok/native";
import { Redirect, Slot } from "expo-router";
import { ActivityIndicator } from "react-native";
import { BluetoothWorkspace } from "../../src/features/bluetooth/device-state";
export default function PatientLayout() {
const { status } = useSession({ sessions: false });
if (status === "loading") return <ActivityIndicator />;
if (status !== "authenticated") return <Redirect href="/sign-in" />;
return (
<BluetoothWorkspace>
<Slot />
</BluetoothWorkspace>
);
}
Create app/index.tsx to choose the initial 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" />
);
}
Expo Router still registers these screens as routes, but signed-in users are immediately redirected away from sign-in and registration. The protected layout also redirects a signed-out user before mounting the Bluetooth provider.
Add consent at the right point
Insert your product's approved consent and privacy step before registration or before entering the monitoring workflow, according to the intended purpose and the decisions made by your clinical and legal teams. The SDK does not supply consent language or decide when consent is legally required. Do not treat a generic checkbox as a substitute for your approved consent flow.
Important Ovok decisions
- Patient self-registration needs
PATIENT_REGISTRATION_ENABLEDenabled and a project default Patient AccessPolicy. Confirm both in patient registration setup before testing. - Patient sign-in uses the sandbox tenant code. Keep it in public app configuration; it is not a password or secret.
- The built-in forms manage their own supported MFA behavior. Your route guard uses
useSession; it does not authorize individual FHIR operations. - A Patient profile is created by the registration workflow. Only add a separate profile-initialization step if your project requires additional fields and the current SDK supports that workflow.
Expected result
A signed-out person can register or sign in with the sandbox project, and a successful login opens the patient route group. A signed-in person cannot remain on the auth screens, and a signed-out person cannot mount the patient Bluetooth workspace.
Common errors and troubleshooting
- Registration returns a project configuration error: confirm patient registration is enabled and the default Patient AccessPolicy exists.
- The form rejects a valid account: verify the tenant code and API URL belong to the same sandbox project.
- The route stays on the loading indicator: inspect
useSession's error state in a development-only diagnostic screen; do not treat a failed refresh as proof of a valid session. - Registration shows an invitation message instead of entering the app: check whether the project routes business-email registrations to clinician invitations and handle that next step intentionally.
Next step
Continue to step 7: build the measurement screen.