> ## 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.

# React Native quickstart

> Use @neurofit/vitals-react-native, a thin TypeScript wrapper over the native iOS and Android SDKs.

`@neurofit/vitals-react-native` is a thin wrapper: every call is forwarded to the native `NeurofitVitals`
(Swift) or `com.neurofit.vitals` (Kotlin) SDK, and every event and result is marshalled to a plain JavaScript
object. There is no reading logic in JavaScript. The module is TurboModule-compatible (`NativeNeurofitVitals.ts`)
and works on the legacy bridge and, through the interop layer, on the New Architecture. React Native 0.73 or
newer.

<Steps>
  <Step title="Install the package">
    The package is delivered with your license. Install it from the tarball or from your private registry.

    ```bash theme={null}
    npm install ./neurofit-vitals-react-native-1.0.0.tgz
    cd ios && pod install
    ```

    The podspec links `NeurofitVitals.xcframework` from the package's `ios/Frameworks/` folder (binary license)
    or the Swift package sources (source license). The Android module depends on `com.neurofit:vitals-sdk:1.0.0`;
    make that artifact resolvable from `mavenLocal()` or your artifact repository, and set `minSdkVersion` to 28
    or higher.
  </Step>

  <Step title="Platform setup">
    **iOS**: add `NSCameraUsageDescription` to `Info.plist` (see the [iOS quickstart](/quickstart-ios)). Adding
    `NSMotionUsageDescription` as well is recommended, because the SDK reads the accelerometer during a reading.

    **Android**: the wrapper's manifest declares `android.permission.CAMERA`, so nothing to add. See the
    [Android quickstart](/quickstart-android) for the optional `uses-feature` entries.
  </Step>

  <Step title="Activate and check the device">
    ```typescript theme={null}
    import NeurofitVitals from '@neurofit/vitals-react-native';

    export async function prepareVitals(licenseKey: string): Promise<boolean> {
      await NeurofitVitals.activate(licenseKey);   // rejects with code 'licenseInvalid' on a bad key

      const support = await NeurofitVitals.deviceSupport();
      if (!support.isSupported) {
        console.warn('Vitals not supported on this device:', support.reason);
        return false;
      }

      if (await NeurofitVitals.hasCameraPermission()) return true;
      return NeurofitVitals.requestCameraPermission();   // resolves to true when granted
    }
    ```

    A rejected promise carries `code` (one of the `VitalsErrorCode` values below) and `message`. See
    [License keys](/license-keys).
  </Step>

  <Step title="Run a reading">
    The wrapper runs one session at a time: `start()` creates it, and it ends with `completed`, `failed` or
    `stop()`. Add listeners **before** calling `start()`; the wrapper does not buffer events.

    ```typescript theme={null}
    import NeurofitVitals, {
      LivePreview,
      VitalsResult,
      VitalsSubscription,
    } from '@neurofit/vitals-react-native';

    export type ReadingHandlers = {
      onGuidance(guidance: string, startProgress: number): void;
      onPreview(preview: LivePreview): void;
      onCancelled(reason: string): void;
    };

    export async function runReading(handlers: ReadingHandlers): Promise<{
      result: Promise<VitalsResult>;
      dispose(): Promise<void>;
    }> {
      const subs: VitalsSubscription[] = [];
      const result = new Promise<VitalsResult>((resolve, reject) => {
        subs.push(
          NeurofitVitals.addListener('guidance', (e) => handlers.onGuidance(e.guidance, e.startProgress)),
          NeurofitVitals.addListener('preview', (e) => handlers.onPreview(e)),   // rmssdMs from about 20 s
          NeurofitVitals.addListener('cancelled', (e) => handlers.onCancelled(e.reason)),   // starts again by itself
          NeurofitVitals.addListener('completed', (e) => resolve(e.result)),
          NeurofitVitals.addListener('failed', (e) => reject(e.error)),
        );
      });

      await NeurofitVitals.start();   // camera + torch on, then positioning

      return {
        result,
        async dispose() {
          subs.forEach((s) => s.remove());
          await NeurofitVitals.stop();   // idempotent; torch off, session released
        },
      };
    }
    ```

    In a component, start the reading in an effect and dispose in its cleanup:

    ```tsx theme={null}
    useEffect(() => {
      let reading: Awaited<ReturnType<typeof runReading>> | undefined;
      runReading({
        onGuidance: (g, p) => { setGuidance(g); setStartProgress(p); },
        onPreview: (preview) => setLive(preview),
        onCancelled: () => setNotice("Let's try that again."),
      }).then((r) => {
        reading = r;
        r.result.then(setResult).catch(setFailure);
      }).catch(setFailure);
      return () => { reading?.dispose(); };
    }, []);
    ```

    `NeurofitVitals.cancelReading()` cancels the reading in progress (a `cancelled` event with reason
    `cancelledByApp`; the session returns to positioning). It is ignored while positioning. `stop()` tears the
    camera down and releases the session; it is idempotent. Once `measurementComplete` has arrived, a stopped
    session still delivers its `completed` (or `failed`) event to your listeners.

    `start()` rejects with `notActivated`, `cameraPermissionDenied`, `unsupportedDevice`, on iOS also
    `cameraUnavailable`, or `internalError` while a session is already running. On Android a camera that cannot
    be opened arrives afterwards as a `failed` event with code `cameraUnavailable`.
  </Step>
</Steps>

## Foreground, orientation and screen

A reading needs the app in the foreground for the whole minute, but leaving it does not end the session.
Backgrounding, or another app taking the camera, cancels a reading in progress with a `cancelled` event whose
`reason` is `interrupted`, and the session resumes positioning by itself when the app is back. Lock the reading
screen's orientation while a session runs (an Android rotation recreates the activity) and keep the screen awake
(for example with `expo-keep-awake` or `react-native-keep-awake`).

To keep the camera positioning without starting a reading (an intro or a sheet covers the screen, say), call
`NeurofitVitals.setAutoStartEnabled(false)`, and `setAutoStartEnabled(true)` when the user is ready. A reading
already measuring is unaffected. The setting applies to the current session and to every later `start()` until
you change it.

## Camera preview

The camera preview is optional on every platform: a fingertip covers the lens during a reading. The V1 wrapper
does not render it; show the guidance text and the live values in your own UI. A preview component is planned;
see [Roadmap](/roadmap).

## API

```typescript theme={null}
activate(licenseKey: string): Promise<void>
deviceSupport(): Promise<DeviceSupport>
hasCameraPermission(): Promise<boolean>                    // never prompts
requestCameraPermission(): Promise<boolean>                // true when granted
start(): Promise<void>
stop(): Promise<void>
cancelReading(): Promise<void>
setAutoStartEnabled(enabled: boolean): Promise<void>      // default true
addListener<Name extends VitalsEventName>(event: Name, callback: (payload: VitalsEventPayload<Name>) => void): VitalsSubscription
```

All of these are exported as named functions and as the `NeurofitVitals` default export.

## Event and result shapes

Event names and payload keys are shared with the Capacitor and Flutter wrappers. Every payload carries `event`;
every documented key is always present, with `null` for an absent optional.

```typescript theme={null}
export type Guidance = 'noContact' | 'lowQuality' | 'weakSignal' | 'compliant' | 'frameDrop';
export type CancelReason = 'contactLost' | 'poorSignal' | 'interrupted' | 'cancelledByApp';
export type SignalLevel = 'strong' | 'good' | 'fair' | 'weak';
export type SignalQuality = 'clean' | 'usable' | 'withheld';
export type BreathingRateConfidence = 'high' | 'medium' | 'low';

export type VitalsErrorCode =
  | 'notActivated' | 'licenseInvalid' | 'cameraPermissionDenied' | 'cameraUnavailable'
  | 'unsupportedDevice' | 'noHeartRate' | 'internalError';

export interface VitalsError { code: VitalsErrorCode; message: string; }

export interface DeviceSupport {
  isSupported: boolean;
  reason: string | null;                     // why the device is not supported; null when supported
}

export interface LivePreview {
  progress: number;                          // 0..1 of the reading's 60 seconds
  heartRateBpm: number | null;
  rmssdMs: number | null;                    // provisional HRV (RMSSD), from about 20 s; null before
  signalLevel: SignalLevel | null;           // ready to display; null until the reading's first scored preview
}

export interface VitalsResult {
  id: string;                                // UUID
  capturedAt: string;                        // ISO 8601
  heartRateBpm: number;                      // always present on a completed reading
  rmssdMs: number | null; sdnnMs: number | null;
  baevskyStressIndex: number | null; sd2sd1: number | null;
  breathingRateBrpm: number | null;
  breathingRateConfidence: BreathingRateConfidence | null;   // null exactly when breathingRateBrpm is null
  quality: SignalQuality;
  sdkVersion: string;
}
```

| Event | Payload keys (besides `event`) |
| - | - |
| `guidance` | `guidance`, `startProgress` (0 to 1) |
| `started` | none |
| `preview` | the `LivePreview` keys, flattened into the payload. About once per second while measuring, plus one at progress 0 right after `started` |
| `heartbeat` | `ibiMs` (number or `null`) |
| `cancelled` | `reason` (`CancelReason`). The session returns to positioning and starts again by itself |
| `measurementComplete` | none |
| `completed` | `result` (`VitalsResult`) |
| `failed` | `error` (`VitalsError`) |

Typed payloads are exported as `GuidanceEvent`, `StartedEvent`, `PreviewEvent`, `HeartbeatEvent`,
`CancelledEvent`, `MeasurementCompleteEvent`, `CompletedEvent` and `FailedEvent`, with the `VitalsEvent` union and
the `VitalsEventPayload<Name>` helper.

## Next steps

* [Reading lifecycle](/reading-lifecycle): what each event means and when it fires.
* [Metrics](/metrics): field definitions, units and validated accuracy.
* [Troubleshooting](/troubleshooting): permission, torch and frame-rate issues.


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