Skip to main content

HealthKit and Health Connect

Use the data sync module to import selected records from Apple HealthKit on iOS or Health Connect on Android. The authorization provider handles the platform permission flow; its matching sync component reads records and saves them as OVOK observations for the signed-in patient.

Before adding the integration, complete installation and native setup and mount the app inside the authenticated OVOK provider tree. Health sync needs an initialized client and a patient profile, and it runs in a native development build rather than Expo Go.

Pick the platform API​

PlatformAuthorization providerImport componentNative source
iOSDataSync.AppleHealthAuthorizationProviderDataSync.AppleHealthSyncHealthKit
AndroidDataSync.AndroidHealthConnectAuthorizationProviderDataSync.AndroidHealthSyncHealth Connect

Import the module from @ovok/native/data-sync. The optional, adapter-driven HealthImportCard is a separate UI primitive; it does not replace these providers or run this import pipeline.

1. Map records to OVOK observations​

Each dataToSync entry connects an OVOK measurement key to one or more platform record identifiers and an observation code. Use the platform's identifier values and a code that matches the value and unit you intend to store.

import { MeasurementTypeKey, ObservationCode } from "@ovok/core";
import { HKQuantityTypeIdentifier } from "@kingstinct/react-native-healthkit";
import type { AppleHealthSyncProps } from "@ovok/native/data-sync";

export const appleDataToSync: AppleHealthSyncProps["dataToSync"] = {
[MeasurementTypeKey.bodyWeight]: {
typeIdentifiers: [
{
typeIdentifier: HKQuantityTypeIdentifier.bodyMass,
code: ObservationCode.BODY_WEIGHT,
},
],
minDate: new Date("2025-01-01T00:00:00.000Z"),
},
};

Android uses Health Connect record names for the same mapping:

import { MeasurementTypeKey, ObservationCode } from "@ovok/core";
import type { AndroidHealthSyncProps } from "@ovok/native/data-sync";

export const androidDataToSync: AndroidHealthSyncProps["dataToSync"] = {
[MeasurementTypeKey.bodyWeight]: {
typeIdentifiers: [
{ typeIdentifier: "Weight", code: ObservationCode.BODY_WEIGHT },
],
minDate: new Date("2025-01-01T00:00:00.000Z"),
},
};

The first read defaults to the most recent 30 days on both platforms. Set minDate to choose an earlier initial boundary. Later runs use the latest matching observation already stored for the patient and provider as their starting point. Daily totals and summaries re-read the latest day so a completed period can be updated. The SDK also keeps provider pagination anchors; anchors persist across app restarts when the OvokClient has storage configured.

The SDK has built-in modes for common data shapes:

modeResult
pointOne observation per source sample or record. This is the default for most measurement keys.
daily-totalOne total for each completed day. This is the default for step count, distance, active energy, flights climbed, and exercise minutes.
daily-summaryOne daily summary. Heart rate defaults to an average with minimum and maximum components.
sessionOne observation for each session. Sleep and workout default to this mode.

The exported HealthDataType enum includes height, bodyFatPercentage, bodyMassIndex, leanBodyMass, distance, activeEnergy, flightsClimbed, exerciseMinutes, heartRateSummary, sleep, and workout. Use these keys for data that is not represented by a core measurement key. Set mode on an entry to override its default.

2. Configure native permissions​

Add the SDK config plugin for the platform integrations your app uses. For an import-only flow, request only read permissions for the record types in dataToSync:

plugins: [
[
"@ovok/native",
{
healthKit: true,
healthConnect: true,
},
],
],
ios: {
infoPlist: {
NSHealthShareUsageDescription:
"Explain why the app reads the selected health data.",
},
},
android: {
permissions: ["android.permission.health.READ_WEIGHT"],
},

For HealthKit background delivery, use healthKit: { background: true } and add the background delivery option in step 5. Add NSHealthUpdateUsageDescription and HealthKit write permissions only if your app also writes data back to the health store. For Health Connect, declare the matching android.permission.health.READ_* permissions for every record type you request. The plugin composes the platform libraries; native config changes take effect after you rebuild the app.

See installation and native setup for plugin details, usage descriptions, and development builds.

3. Mount the authorization provider and sync component​

The provider requests access automatically when a request is needed. Pass only the identifiers the feature will read. For import-only flows, omit writeIdentifiers. The mapping objects, status setters, progress state, and error handler below are app-owned values; define them in the screen or feature module.

import * as React from "react";
import { ActivityIndicator } from "react-native";
import { DataSync } from "@ovok/native/data-sync";
import { HKQuantityTypeIdentifier } from "@kingstinct/react-native-healthkit";

export function AppleHealthImport() {
return (
<DataSync.AppleHealthAuthorizationProvider
readIdentifiers={[HKQuantityTypeIdentifier.bodyMass]}
fallback={<ActivityIndicator />}
onAuthorizationStatusChange={setHealthKitStatus}
>
<DataSync.AppleHealthSync
dataToSync={appleDataToSync}
onProgress={setProgress}
onError={handleSyncError}
/>
</DataSync.AppleHealthAuthorizationProvider>
);
}

For Health Connect, use its record identifiers and provider:

import { ActivityIndicator } from "react-native";
import { DataSync } from "@ovok/native/data-sync";

export function HealthConnectImport() {
return (
<DataSync.AndroidHealthConnectAuthorizationProvider
readIdentifiers={["Weight"]}
fallback={<ActivityIndicator />}
onAuthorizationStatusChange={setHealthConnectStatus}
>
<DataSync.AndroidHealthSync
dataToSync={androidDataToSync}
onProgress={setProgress}
onError={handleSyncError}
/>
</DataSync.AndroidHealthConnectAuthorizationProvider>
);
}

Both sync components read the active client and patient from @ovok/core. Mount them only after the user's session and patient profile are ready. The provider controls the permission UI; the sync component starts its import when mounted.

Own the permission screen​

Provide both renderManualRequestUI and skipRequest when your app owns the prompt. Set skipRequest={false} to let the provider render the callback UI and avoid automatic permission requests:

import { Button } from "react-native";
import { DataSync } from "@ovok/native/data-sync";

<DataSync.AndroidHealthConnectAuthorizationProvider
readIdentifiers={["Weight"]}
skipRequest={false}
renderManualRequestUI={(requestAccess) => (
<Button title="Continue" onPress={requestAccess} />
)}
>
<DataSync.AndroidHealthSync dataToSync={androidDataToSync} />
</DataSync.AndroidHealthConnectAuthorizationProvider>

The callback receives the provider's requestAccess function. To build a bottom sheet with the supplied pieces, see ManuallyRequestSheet. A Skip action is app behavior: update the screen or navigation and do not leave an import component mounted behind a skipped permission prompt. Setting skipRequest={true} bypasses the provider's request gate and renders its children.

Apple does not expose whether a person denied read access to a particular HealthKit type. Its status reports types that still need a response, but a ready state is not proof that every requested type is readable. Treat empty reads as a valid outcome.

4. Render progress and handle errors​

The sync components report progress as a partial map keyed by the configured measurement key. Render it with the exported SyncProgressList or provide your own UI:

import { SyncProgressList } from "@ovok/native/data-sync";

<SyncProgressList
progress={progress}
labelForType={(type) => measurementLabels[type] ?? type}
statusLabels={{ syncing: "Importing", completed: "Imported" }}
/>;

SyncProgress includes measurementTypeKey, status, optional pause reason, and optional progress.done / progress.total counts. Status values are idle, started, syncing, paused, failed, and completed. Pause reasons are offline and wifi-only.

chunkSize defaults to 250 records per provider read. wifiOnly defaults to false; when the device is offline, the sync pauses, and when wifiOnly is true, it also pauses on non-Wi-Fi networks. The sync resumes when the connection meets the configured policy.

onError receives a Bundle<Resource>, an Error, or null. Narrow the value before reading error-specific fields, and report failures through the app's normal error handling.

5. Add background delivery only when needed​

For HealthKit observer updates, pass backgroundDelivery to AppleHealthSync and enable the HealthKit background capability in the config plugin. Background delivery asks iOS to notify the app about new samples; the OS controls when the app runs.

For Android Health Connect imports after the app process stops, use the registered headless task and scheduler in the background sync guide. Set backgroundAccess on the authorization provider to request Health Connect's separate background-read permission. That permission is reported independently from foreground access, and declining it does not block foreground imports.

Do not render AndroidHealthSync inside a headless task. The headless API runs an import with an explicitly restored client, patient ID, and mapping.

Component references​

For a separate scheduled Android import, continue with background sync.