> ## 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 while `motionBreathing` is on.

    **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> {
      const status = await NeurofitVitals.activate(licenseKey);      // rejects on an invalid key
      status.warnings.forEach((w) => console.warn('License warning:', w));

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

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

    A rejected promise carries `code` (the native error case: `licenseInvalid`, `licenseNotValidForApp`,
    `licenseUpdatesExpired`, `cameraDenied`, ...) and `message`. See [License keys](/license-keys).
  </Step>

  <Step title="Run a reading">
    `createSession` returns an opaque session handle. Subscribe to events **before** calling `start`; the wrapper
    does not buffer events. Every payload carries `event` and `sessionId`, so one set of listeners can serve
    several sessions if you ever need that.

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

    export type ReadingHandlers = {
      onGuidance(guidance: string, startProgress: number): void;
      onPreview(elapsed: number, progress: number, hrBpm: number | null, rmssdMs: number | null): void;
      onCancelled(reason: string): void;
      onLowPowerMode(): void;
    };

    export async function runReading(handlers: ReadingHandlers): Promise<{
      session: VitalsSessionHandle;
      result: Promise<VitalsResult>;
      dispose(): Promise<void>;
    }> {
      const session = await NeurofitVitals.createSession({
        readingDuration: 60,
        autoRestartAfterCancel: true,
        motionBreathing: true,
      });

      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.elapsed, e.progress, e.hrBpm, e.rmssdMs)),
          NeurofitVitals.addListener('cancelled', (e) => handlers.onCancelled(e.reason)),   // session re-arms itself
          NeurofitVitals.addListener('reconfiguringCamera', (e) => { if (e.lowPowerMode) handlers.onLowPowerMode(); }),
          NeurofitVitals.addListener('completed', (e) => resolve(e.result)),
          NeurofitVitals.addListener('failed', (e) => reject(e.error)),
        );
      });

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

      return {
        session,
        result,
        async dispose() {
          subs.forEach((s) => s.remove());
          await NeurofitVitals.stop(session);   // idempotent; torch off, handle 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: (elapsed, progress, hrBpm, rmssdMs) => setLive({ elapsed, progress, hrBpm, rmssdMs }),
        onCancelled: (reason) => setNotice(`Reading restarted: ${reason}`),
        onLowPowerMode: () => setNotice('Please turn off Low Power Mode.'),
      }).then((r) => {
        reading = r;
        r.result.then(setResult).catch(setFailure);
      });
      return () => { reading?.dispose(); };
    }, []);
    ```

    `NeurofitVitals.cancelReading(session, 'user_tapped_cancel')` cancels the current attempt while measuring (the
    session returns to positioning; the call is ignored while positioning). `stop(session)` tears the camera down
    and releases the handle; it is idempotent, so a second `stop` resolves. A later `start` or `cancelReading` with
    a released handle rejects with the wrapper-level code `sessionNotFound`.

    `start` rejects with the native error (`notActivated`, `alreadyRunning`, `cameraDenied`, `cameraRestricted`,
    `unsupportedDevice`, on iOS also `cameraUnavailable`); 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: backgrounding (iOS) or the host activity's
`ON_STOP` (Android) during positioning or measuring ends the session with a `failed` event whose `error.code` is
`interrupted`. 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`).

## 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<LicenseStatus>
deviceSupport(): Promise<DeviceSupport>
requestCameraPermission(): Promise<boolean>
createSession(config?: VitalsConfiguration): Promise<VitalsSessionHandle>   // string handle
start(session: VitalsSessionHandle): Promise<void>
cancelReading(session: VitalsSessionHandle, reason?: string): Promise<void>  // reason defaults to "host"
stop(session: VitalsSessionHandle): Promise<void>
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`
and `sessionId`; 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' | 'lowQuality' | 'lowLiveSignalQuality' | 'noLowDiscardEstimate' | 'host';
export type SignalLevel = 'strong' | 'good' | 'fair' | 'weak';
export type AbortRisk = 'none' | 'warn' | 'imminent';
export type SignalQuality = 'clean' | 'usable' | 'withheld';
export type RecoverableErrorKind = 'frameRateUnsustainable' | 'cameraStalled';
export type VitalsSessionStateName = 'idle' | 'positioning' | 'measuring' | 'finalizing' | 'completed' | 'failed';

export type VitalsErrorCode =
  | 'notActivated' | 'licenseInvalid' | 'licenseNotValidForApp' | 'licenseUpdatesExpired'
  | 'cameraDenied' | 'cameraRestricted' | 'cameraUnavailable' | 'unsupportedDevice'
  | 'interrupted' | 'noHeartRate' | 'alreadyRunning' | 'internalError'
  | 'sessionNotFound';                       // wrapper-level: unknown or already stopped handle

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

export interface LicenseStatus {
  licensee: string; apps: string[]; features: string[];
  updatesUntil: string;                      // YYYY-MM-DD
  type: 'binary' | 'source'; warnings: string[];
}

export interface DeviceSupport {
  isSupported: boolean; hasCamera: boolean; hasTorch: boolean;
  fpsTier: number | null;                    // highest reachable tier: 60 / 30 / 20 on iOS, 30 on Android
  reason: string | null;
}

export interface VitalsConfiguration {      // every key optional; omitted keys keep the production defaults
  readingDuration?: number;                  // seconds, default 60
  autoRestartAfterCancel?: boolean;          // default true
  motionBreathing?: boolean;                 // default true
  advanced?: AdvancedConfiguration;          // camelCase of the parameters on the Reading lifecycle page
}

export interface LivePreview {
  elapsed: number; progress: number;         // seconds; 0..1
  hrBpm: number | null;
  rmssdMs: number | null;                    // provisional HRV (RMSSD), from about 20 s; null before
  signalLevel: SignalLevel;
  breathingRate: number | null;              // provisional, from about 30 s; null before
  abortRisk: AbortRisk;
}

export interface VitalsResult {
  id: string;                                // UUID
  capturedAt: string;                        // ISO 8601
  durationSeconds: number;
  heartRateBpm: number | null; rmssdMs: number | null; sdnnMs: number | null;
  baevskyStressIndex: number | null; sd2sd1: number | null;
  breathingRateBrpm: number | null; breathingRateConfidence: number | null;
  confidence: number;
  quality: SignalQuality;
  flags: string[];
  diagnostics: Record<string, number | string | boolean>;   // snake_case keys, see Diagnostics
  engineVersion: string; sdkVersion: string;
}
```

| Event | Payload keys (besides `event`, `sessionId`) |
| - | - |
| `state` | `state` (`VitalsSessionStateName`); `elapsed` while measuring; `result` when completed; `error` when failed. Emitted on transitions only: the per-second measuring clock is in the `preview` payload, not in `state` events |
| `guidance` | `guidance`, `startProgress` (0 to 1) |
| `preview` | the `LivePreview` keys, flattened into the payload |
| `heartbeat` | `ibiMs` (number or `null`) |
| `started` | none |
| `cancelled` | `reason` (`CancelReason`), `hostReason` (string when `reason` is `host`, else `null`), `context` (the native `CancelContext` map) |
| `reconfiguringCamera` | `lowPowerMode` (boolean) |
| `recoverableError` | `kind` (`RecoverableErrorKind`), `fps` (number for `frameRateUnsustainable`, else `null`). One automatic recovery per session; a second recoverable error arrives as `failed` |
| `measurementComplete` | none |
| `completed` | `result` (`VitalsResult`) |
| `failed` | `error` (`VitalsError`) |

Typed payloads are exported as `StateEvent`, `GuidanceEvent`, `PreviewEvent`, `HeartbeatEvent`, `StartedEvent`,
`CancelledEvent`, `ReconfiguringCameraEvent`, `RecoverableErrorEvent`, `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.