> ## Documentation Index
> Fetch the complete documentation index at: https://developer.neurofit.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Pre-built reading flow

> The optional, drop-in reading screens for React Native and Flutter: what each screen shows, the callbacks, the camera preview rules, and every colour and string you can override.

`VitalsReadingFlow` is a complete, full-screen reading for React Native and Flutter, in the style of the NEUROFIT
app. It uses only the wrapper's public API, adds no dependency and needs no setup beyond the platform setup and
`activate`. Present it with the [React Native](/quickstart-react-native#pre-built-reading-flow) or
[Flutter](/quickstart-flutter#pre-built-reading-flow) quickstart. This page is the reference. The flow is a preview
in 1.0.0.

<div className="flow-screens">
  <figure>
    <img src="https://mintcdn.com/neurofit/l6iTAovd__JNk2DH/images/reading-flow-tips.png?fit=max&auto=format&n=l6iTAovd__JNk2DH&q=85&s=786d32d992569ee8724e931aefae1e90" alt="Tips: five placement tips and a Continue button" width="603" height="1223" data-path="images/reading-flow-tips.png" />

    <figcaption>Tips</figcaption>
  </figure>

  <figure>
    <img src="https://mintcdn.com/neurofit/l6iTAovd__JNk2DH/images/reading-flow-positioning.png?fit=max&auto=format&n=l6iTAovd__JNk2DH&q=85&s=46ee0abff2351985f3261d2c0d0c7408" alt="Positioning: the ring at 60 seconds over the camera, and a card that says the reading is starting, with its start bar more than half full" width="603" height="1223" data-path="images/reading-flow-positioning.png" />

    <figcaption>Positioning</figcaption>
  </figure>

  <figure>
    <img src="https://mintcdn.com/neurofit/l6iTAovd__JNk2DH/images/reading-flow-reading.png?fit=max&auto=format&n=l6iTAovd__JNk2DH&q=85&s=46af5d76d69ff48dca5cd8c6a9070c90" alt="Reading: the ring shows 36 seconds left, and the live strip shows heart rate 64 bpm, signal Good and HRV 42 ms" width="603" height="1223" data-path="images/reading-flow-reading.png" />

    <figcaption>Reading</figcaption>
  </figure>

  <figure>
    <img src="https://mintcdn.com/neurofit/l6iTAovd__JNk2DH/images/reading-flow-results.png?fit=max&auto=format&n=l6iTAovd__JNk2DH&q=85&s=6562739356f961e4c3f24c105721d4f3" alt="Results: heart rate, HRV (RMSSD), HRV (SDNN), stress index and breathing rate, with a Done button" width="603" height="1223" data-path="images/reading-flow-results.png" />

    <figcaption>Results</figcaption>
  </figure>
</div>

The default light theme on the iOS Simulator, with sample values. The simulator has no camera, so a flat colour
stands in for the camera preview.

## Screens

| Screen | Shows | Leads to |
| - | - | - |
| Tips | How to take a good reading, with a Continue button. Shown first while `showTips` is true. | Camera access, or positioning |
| Camera access | Why the camera is needed and that images never leave the phone. Allow camera asks. After a denial, Open settings goes to the app's settings, and returning with access granted continues. | Positioning |
| Bright light | Only on a phone without a torch (`hasTorch` false), once: readings need bright, even light. | Positioning |
| Positioning | The live camera preview under a light scrim, the progress ring at 60 seconds, and a card with the guidance for the current fingertip placement. As the signal steadies, a bar under the guidance fills, and the reading starts by itself when it is full. A Reading tips link opens the tips over the preview. | Reading |
| Reading | The ring counts down the 60 seconds and pulses with each heartbeat. The live strip shows heart rate, the signal level and HRV once they are measured. | Checking |
| Checking | About 1 to 3 seconds while the SDK checks the reading. | Results, or positioning with a note |
| Paused | After 3 readings in a row that fail the quality check: the tips again, with Try again. | Positioning |
| Results | Heart rate, HRV (RMSSD), SDNN, the stress index when present and the breathing rate in breaths/min (marked approximate for the `low` tier, hidden when `withheld`), with a Done button. Skipped with `showResults` false. | `onComplete` |
| Not available | An unsupported device, a camera in use, a missing or invalid license, or an internal error, each with calm, specific text. | Try again, or `onError` |

When a reading is cancelled (the fingertip moved, the signal was poor, or the reading failed the quality check), the
flow returns to positioning with a short note in the card, and the SDK starts the next reading by itself. After an
interruption (the app went to the background, or another app took the camera) the card says the reading starts again
by itself.

If the paused screen or the tips stay open for 2 minutes, the flow turns the camera and torch off and lets the screen
lock. Try again or Continue turns them back on.

## Callbacks

Exactly one callback fires per mount, when the user leaves the flow. The flow then keeps showing its last screen,
frozen, until you remove it.

| Callback | When |
| - | - |
| `onComplete(result)` | The user leaves the results screen (Done, close or back), or at once with `showResults` false |
| `onCancel()` | The user closes the flow, or goes back, before a result |
| `onError(error)` | The user leaves an error screen they saw: `unsupportedDevice`, `cameraUnavailable`, `notActivated`, `licenseInvalid` or `internalError` |

Removing the flow before then (your navigation, an iOS swipe back) stops the session and calls nothing. Every exit
leaves `setAutoStartEnabled(true)` behind. Activation is your app's job: without it the flow shows that readings
aren't available right now and ends with `onError` (`notActivated`).

System back closes the tips first, then leaves as the close button does. In React Native, inside a `Modal` on
Android, back goes to the Modal's `onRequestClose`: forward it with the flow's ref,
`onRequestClose={() => flowRef.current?.back()}`. In Flutter the flow refuses the route pop and handles back itself,
so the iOS swipe back is off while it shows. The close button and the callbacks cover it.

## Presenting the flow

* Full screen, with no navigation header: a React Native `Modal` or a header-less screen, or a Flutter route with no
  app bar. The flow draws its own header under the status bar.
* In a portrait-locked screen. The SDK asks for a locked orientation during a reading.
* One flow at a time: the wrapper runs one session.
* Collect any consent your app needs before you show the flow. The flow explains the camera, not your app's
  handling of results.
* React Native: the flow needs the New Architecture (the default since React Native 0.76). It reads the safe-area
  insets of the app's window from the native module. Pass `safeAreaInsets` to set them yourself, for example in a
  Modal with `statusBarTranslucent` in an Android app that does not draw edge to edge.

## Camera preview

`VitalsCameraPreview` is a native view of the wrapper session's camera: the session's `previewLayer` on iOS, and on
Android a CameraX `PreviewView` the session binds at `start()`. The flow mounts it before it calls `start()` and keeps
the same one mounted from positioning to the end of the reading. Do the same in your own layout:

* Mount the preview, wait for its `onReady`, then call `start()`. On Android the session binds its preview to the view
  that exists at `start()`. If it never reports ready, the flow starts after 1.5 seconds without a preview.
* One preview shows the camera at a time: the last one mounted.
* It keeps the screen awake while it is shown, and it is hidden from screen readers (a fingertip covers the lens).

## Theme

Every colour, the font family and the corner radii can be overridden, for light and dark separately. React Native
takes partial objects (`theme={{ cornerRadius: 12, light: { accent: '#0055FFFF' } }}`), Flutter takes
`defaultVitalsTheme.copyWith(...)` with `VitalsColors.copyWith(...)`. `appearance` is `system` (the default, which
follows the phone), `light` or `dark`.

| Option | Default |
| - | - |
| `fontFamily` | `Jost`: the bundled Jost (SIL Open Font License 1.1), registered with no setup. Any other family is used as given |
| `cornerRadius` | `0` (cards and buttons) |
| `liveStripCornerRadius` | `12` |

The bundled Jost ships with its license and needs no font setup in your app. React Native registers it on Android
under the private family name `NeurofitVitalsJost`, so a font your app calls `Jost` is never replaced. On iOS, if
another font in your app already uses Jost's names, the React Native flow uses the system font throughout.

Colours are `#RRGGBBAA` strings in React Native and `Color(0xAARRGGBB)` in Dart.

| Token | Light | Dark | Used for |
| - | - | - | - |
| `background` | `#EDEEF2FF` | `#16181EFF` | Text screens, results, tips, paused |
| `surface` | `#FFFFFFFF` | `#282C36FF` | Header, cards |
| `textPrimary` | `#000000FF` | `#F2F3F5FF` | Text |
| `textSecondary` | `#747A8CFF` | `#9AA0ADFF` | The approximate breathing rate (large text) |
| `textMuted` | `#5A616BFF` | `#9AA0ADFF` | Small secondary text: strip titles and units, notes |
| `divider` | `#000000FF` | `#343A45FF` | The line under the header |
| `shadow` | `#DDDDDDFF` | `#00000073` | Card shadow |
| `accent` | `#000000FF` | `#F2F3F5FF` | The start bar. Your brand colour goes here |
| `track` | `#DEE0E7FF` | `#3A3F4AFF` | The start bar's track |
| `controlSolidBackground` | `#000000FF` | `#4E5561FF` | Buttons |
| `controlSolidForeground` | `#FFFFFFFF` | `#F2F3F5FF` | Button text |
| `controlSolidBorder` | `#00000000` | `#626A78FF` | Button edge |
| `previewScrim` | `#EDEEF266` | `#16181E66` | Over the camera preview |
| `liveStripBackground` | `#FFFFFFE6` | `#282C36E6` | The live strip |
| `liveStripDivider` | `#DDDDDDFF` | `#4E5561FF` | Lines between the strip's values |
| `liveStripShadow` | `#0000001F` | `#0000001F` | The strip's shadow |
| `ringTrack` | `#EDEEF2FF` | `#EDEEF2FF` | The ring's track |
| `ringProgress` | `#000000FF` | `#282C36FF` | The ring's arc |
| `ringDisc` | `#000000FF` | `#282C36FF` | The ring's centre |
| `ringHalo` | `#EDEEF266` | `#EDEEF266` | Behind the ring |
| `ringText` | `#FFFFFFFF` | `#FFFFFFFF` | The seconds in the ring |
| `signalStrong` | `#117D3DFF` | `#54B558FF` | Signal level: Strong |
| `signalGood` | `#437A19FF` | `#93E29AFF` | Signal level: Good |
| `signalFair` | `#9E5E00FF` | `#FFAA32FF` | Signal level: Fair |
| `signalWeak` | `#C11F26FF` | `#F37F7CFF` | Signal level: Weak |

The default text and signal colours meet WCAG AA contrast over the camera preview. If you change them, check the
contrast in both appearances.

## Strings

Every string can be overridden: React Native `strings={{ titleReading: 'Heart check' }}`, Flutter
`defaultVitalsStrings.copyWith(titleReading: 'Heart check')`. The flow fills `{value}` and `{seconds}`. Strings marked
"no torch" replace their neighbour on a phone without a torch.

| Key | English default |
| - | - |
| `titleReading` | Measure vitals |
| `titleResults` | Your results |
| `closeLabel` | Close |
| `continueButton` | Continue |
| `tryAgainButton` | Try again |
| `doneButton` | Done |
| `closeButton` | Close |
| `placeholder` | -- |
| `tipsHeading` | Your reading takes one minute. For the best result: |
| `tipRest` | Sit down and rest your phone on a table or your lap. |
| `tipWarm` | Warm your hands first. A warm fingertip shows your pulse best. |
| `tipCover` | Rest your fingertip lightly over the back camera and flash. Don't press hard. |
| `tipCoverNoTorch` | Rest your fingertip lightly over the back camera. Don't press hard. (no torch) |
| `tipStill` | Keep still and breathe normally until the reading ends. |
| `tipCase` | Remove any case that covers the camera. |
| `tipLight` | Sit in bright, even light, near a lamp or a window. (no torch only) |
| `tipsLink` | Reading tips |
| `permissionHeading` | Camera access |
| `permissionBody` | Readings use your back camera and flash to see the pulse in your fingertip. |
| `permissionBodyNoTorch` | Readings use your back camera to see the pulse in your fingertip. (no torch) |
| `permissionPrivacy` | Camera images are processed on this phone and never stored or sent. |
| `permissionAsk` | To continue, please allow camera access. |
| `permissionDenied` | To continue, please allow camera access in Settings. |
| `allowCameraButton` | Allow camera |
| `openSettingsButton` | Open settings |
| `noTorchHeading` | Find bright, even light |
| `noTorchBody` | Readings on this phone need bright, even light because its camera has no flash. Sit near a lamp or a bright window. |
| `guidanceNoContact` | With a warm fingertip, cover the back camera. Slowly adjust finger pressure until you see your pulse in the red glow. |
| `guidanceNoContactNoTorch` | With a warm fingertip, cover the back camera. Stay in bright, even light. (no torch) |
| `guidanceLowQuality` | Slowly adjust your finger pressure until you can see your pulse in the red glow. |
| `guidanceWeakSignal` | Your pulse is faint. Warm your hands and rest your fingertip lightly. |
| `guidanceFrameDrop` | The camera is running slowly. Turn off Low Power Mode or Battery Saver if it's on. |
| `guidanceCompliant` | Starting your reading. Hold still and keep the same finger pressure. |
| `waitingCamera` | Getting the camera ready. |
| `waitingInterrupted` | Paused. Your reading starts again by itself. |
| `noticeContactLost` | Your fingertip moved. Rest it on the camera to start again. |
| `noticePoorSignal` | Let's try that again. Rest your phone on a table and keep still. |
| `noticeQualityCheckFailed` | Let's take that one again. Keep still and the next reading starts by itself. |
| `measuring` | Measuring your vitals. Keep the same finger pressure, sit still and relax for a minute. |
| `finalizing` | Checking your reading. |
| `ringSecondsLabel` | sec |
| `ringCheckingLabel` | Checking |
| `stripHeartRate` | Heart rate |
| `stripSignal` | Signal |
| `stripHrv` | HRV |
| `unitBpm` | bpm |
| `unitMs` | ms |
| `unitBreathsPerMin` | breaths/min |
| `signalStrong` | Strong |
| `signalGood` | Good |
| `signalFair` | Fair |
| `signalWeak` | Weak |
| `pausedHeading` | Your last 3 readings weren't clear enough to show. |
| `pausedBody` | These tips usually help: |
| `resultsHeartRate` | Heart rate |
| `resultsHrv` | HRV (RMSSD) |
| `resultsSdnn` | HRV (SDNN) |
| `resultsStressIndex` | Stress index |
| `resultsBreathingRate` | Breathing rate |
| `breathingApprox` | about {value} |
| `resultsUsableNote` | Your signal was a little noisy, so these numbers are less precise than usual. |
| `unsupportedHeading` | Readings aren't available on this phone |
| `unsupportedBody` | This phone's camera can't take a reading. |
| `errorCameraHeading` | The camera isn't available |
| `errorCameraBody` | Close other apps that use the camera, then try again. |
| `errorGenericHeading` | Something went wrong |
| `errorGenericBody` | Please try again. |
| `errorUnavailableHeading` | Readings aren't available right now |
| `errorUnavailableBody` | Please try again later. |
| `a11yRingLabel` | Reading progress |
| `a11yRingValue` | {seconds} seconds left |
| `a11yStarted` | Reading started. Keep still for one minute. |
| `a11yUnitBpm` | beats per minute |
| `a11yUnitMs` | milliseconds |
| `a11yUnitBreathsPerMin` | breaths per minute |
| `a11yNotAvailable` | not available yet |
| `a11yApproximate` | approximate |

## Accessibility

Every control and value has a screen-reader label, with units spoken in full. Focus moves to each new screen's
heading (a denied camera request counts as a new screen). The guidance, notes, a reading starting and checking are
announced on iOS and read from a polite live region on Android. Text follows the system text size within a limit per
element, and Reduce Motion turns the animations off.

## Building blocks

For your own layout, the flow's pieces are exported: `VitalsCameraPreview`, `VitalsProgressRing`,
`VitalsGuidanceCard`, `VitalsSignalIndicator`, `VitalsLiveStrip`, `VitalsResultsCard`, `vitalsGuidanceText`,
`vitalsCancelNotice`, `defaultVitalsTheme` and `defaultVitalsStrings`. Each takes the same theme and strings.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.