# Reusable flows

Canonical page: https://docs.ovok.com/native-sdk/guide/how-to/reusable-flows

The `@ovok/native/ui` module includes three higher-level components for common app tasks: pairing a nearby device, collecting a manual measurement, and presenting health-data access through an app-provided adapter. Each component handles its own local UI state. Your app supplies the provider, storage, persistence, permissions, patient context, and navigation that give the flow meaning in your product.

Use the [App UI guide](/native-sdk/guide/modules/ui) for the complete UI catalog, shared theme, and slot customization.

| Flow | Use it when | Your app supplies |
| --- | --- | --- |
| [`PairingFlow`](/native-sdk/guide/components/ui/pairing-flow) | Users need to choose and pair a discovered Bluetooth device | An active `BTProvider`, `PairedDeviceStorage`, and a code handler when the device requests one |
| [`ManualEntryForm`](/native-sdk/guide/components/ui/manual-entry-form) | Users need to enter a measurement defined by the SDK catalog | An async save callback, patient mapping, and product-specific labels |
| [`HealthImportCard`](/native-sdk/guide/components/ui/health-import-card) | You need an access-status and request UI around an existing health adapter | A `HealthImportAdapter`, permission policy, and any separate import service |

Adopt these independently. Pairing and health access do not depend on each other, and manual entry can be used without either provider.

## Pair and remember a Bluetooth device [#pair-and-remember-a-bluetooth-device]

Render `PairingFlow` inside the Bluetooth provider that owns the current device session. The flow presents devices already discovered by that provider; it does not create a BLE manager or start a scan.

~~~tsx
import { BTProvider, IntegratedDevices } from "@ovok/native/bt-management";
import { PairingFlow } from "@ovok/native/ui";

const acceptedDevices = [IntegratedDevices.BP2] as const;

<BTProvider bleManager={bleManager} acceptedDevices={acceptedDevices}>
  <PairingFlow
    storage={pairedDeviceStorage}
    onPaired={(device) => analytics.track("device_paired", { id: device.id })}
    onError={(error, device) => reportPairingError(error, device?.id)}
  />
</BTProvider>
~~~

`storage` implements async `getItem(key)` and `setItem(key, value)` methods. The flow stores a JSON array of device IDs under `@ovok/native/paired-devices` by default; set `storageKey` to keep a separate collection. It does not store credentials or device records.

When the runtime reports that pairing needs a code, the flow displays an input and passes its value and selected device to `onPairingCode`. Provide this callback to run the device- or platform-specific confirmation. A successful callback returns the UI to the pairing state; the device still needs to report that it is bonded before the flow remembers it and calls `onPaired`.

The default device row starts pairing. If users also need to forget remembered devices, render a custom row with `renderDevice`; its `actions` argument includes a `forget()` function. The flow exposes devices, remembered IDs, the selected device, status, and error to custom render callbacks.

## Collect and save a manual measurement [#collect-and-save-a-manual-measurement]

`ManualEntryForm` uses the SDK's `measurementFieldCatalog` for the selected `MeasurementTypeKey`. Required fields, numeric bounds, and units come from that catalog. The form validates values and calls the app's async save function with a plain typed payload:

~~~tsx
import { MeasurementTypeKey } from "@ovok/core/next";
import { ManualEntryForm, type ManualEntrySaveInput } from "@ovok/native/ui";

const saveMeasurement = async (input: ManualEntrySaveInput) => {
  await measurementRepository.save({
    patientId: activePatient.id,
    ...input,
  });
};

<ManualEntryForm
  measurementTypeKey={MeasurementTypeKey.bodyWeight}
  saveMeasurement={saveMeasurement}
  labelForField={(field) => measurementLabels[field] ?? field}
  onSaved={() => navigation.goBack()}
/>
~~~

The app decides which patient owns the entry, how it is persisted or queued, and whether it should be converted into a FHIR resource. The callback must resolve only after the app considers the save complete; `onSaved` runs after it resolves. While the callback is pending, the form blocks another save and shows a loading state. Validation and rejected-save errors appear inline.

`initialValues` and `recordedAt` seed state when the form mounts. If the active measurement changes after that, remount with a new React `key` or manage editing state in the app. The built-in copy is English; provide `title`, `labelForField`, and `labelForUnit` for your product language and units.

## Request health-data access through an adapter [#request-health-data-access-through-an-adapter]

`HealthImportCard` is a status and request surface for an app-owned `HealthImportAdapter`. Use it when the app already owns the platform authorization implementation:

~~~tsx
import { HealthImportCard } from "@ovok/native/ui";
import type { HealthImportAdapter } from "@ovok/native/bt-management";

type HealthType = "steps" | "weight";

const adapter: HealthImportAdapter<HealthType> = {
  getStatus: (type) => readAccessStatus(type),
  request: async (types, background) => {
    await requestHealthAccess(types, { background });
  },
};

<HealthImportCard
  types={["steps", "weight"]}
  adapter={adapter}
  labelForType={(type) => healthTypeLabels[type]}
  progress={progressByType}
/>
~~~

`getStatus` returns `authorized`, `shouldRequest`, `refused`, or `unsupported` for each app-defined type. `request(types, background)` receives the whole type list and the `background` option. On mount and after a successful request, the card reads statuses again; it considers the card enabled only when every listed type is authorized.

Switching the card off only changes its enabled state. The adapter has no revoke, stop, or cancel method, so the control does not disable OS permissions or stop an import service. The component also does not configure HealthKit or Health Connect or start the SDK's `DataSync` pipeline. Use the native permission providers and import components documented in [HealthKit and Health Connect](/native-sdk/guide/modules/data-sync) when you need those integrations.

## Keep app decisions at the app boundary [#keep-app-decisions-at-the-app-boundary]

These flows are building blocks rather than end-to-end product screens. Keep these decisions in app-owned code:

- which devices and measurement types the user can select;
- where device IDs and measurements are stored;
- which patient or account owns each measurement;
- how health access is explained and requested;
- when to navigate after an operation succeeds;
- how errors, analytics, and translations fit the product.

Use the [flow references](/native-sdk/guide/components/ui/reusable-flows/) for each component's props, callbacks, and lifecycle details.
